For production grade applications SOLID are a must principles, but when adding a new feature open for extension close for modification principle could be easily violated.
Hardcoding filters
Let's take an example of an image processing application where we need an image filter plugin system.
The core interface is simple, it must be implemented by classes as a best practice.
Name
represents the filter that we will apply to a given image.
The common approach
The common way is to manually register every known filter
public class FilterManager
{
public IFilter GetFilter(string filterName)
{
switch (filterName.ToLower())
{
case "grayscale":
return new GrayscaleFilter();
case "sepia":
return new SepiaFilter();
// PROBLEM: To add a new filter, we must come back here and modify this file.
/*
case "invert":
return new InvertColorsFilter();
*/
default:
throw new NotSupportedException($"Filter '{filterName}' is not recognized.");
}
}
}
Main application logic:
Why this is a problem?
This approach is technical debt and time consuming because it will introduce:
Open/Close principle violation: The
FilterManager
is not 'closed for modification.' To add a newInvertColorsFilter
, we are forced to open and change theFilterManager
code which is time consuming, overwhelming and could be risky for a production grade level.Error prone: Whenever a new filter is added we should always add it to code to previous switch statement and this creates a tightly coupled application.
Merge conflict: For faster problems solving and project success, team play is needed but as the team grow a single file can become the hotspot where multiple developers edit it simultaneously, which introduces frustrating merge conflicts.
Assembly scanning for discovery as a solution
Refactoring the code for a production-grade level by adding a generic engine that builds classes automatically would be a better approach.
public class FilterManager
{
private readonly IReadOnlyDictionary<string, Type> _discoveredFilters;
public FilterManager(IReadOnlyDictionary<string, Type> discoveredFilters)
{
_discoveredFilters = discoveredFilters;
}
public IFilter GetFilter(string filterName)
{
if (!_discoveredFilters.TryGetValue(filterName.ToLower(), out var filterType))
{
throw new NotSupportedException($"Filter '{filterName}' is not recognized.");
}
return (IFilter)Activator.CreateInstance(filterType)!;
}
}
private static IReadOnlyDictionary<string, Type> BuildFilterCatalog()
{
var catalog = new Dictionary<string, Type>();
var pluginAssembly = typeof(Program).Assembly;
var filterTypes = pluginAssembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && typeof(IFilter).IsAssignableFrom(t));
foreach (var type in filterTypes)
{
var tempFilter = (IFilter)Activator.CreateInstance(type)!;
Console.WriteLine($"[Plugin Discovery] Found filter: '{tempFilter.Name}'");
catalog[tempFilter.Name.ToLower()] = type;
}
return catalog;
}
How it works
discoveredFilters.TryGetValue(...)
: Tries to find an entry in the dictionary using a key.-
pluginAssembly.GetTypes()
: The pluginAssembly have references to .dll and .exe files.-
GetTypes()
: will return every single: - class
- interface
- struct
- enum
-
-
.Where(...)
: Is a Language Integrated Query, used to keep data that satisfy given conditions.-
t.IsClass
: checks if the current type is a class. -
!t.IsAbstract
: filters out abstract classes. -
typeof(IFilter).IsAssignableFrom(t)
: determines if the type can be assigned to a variable of type IFilter.
-
foreach
loop takes a list of objects, each representing a filter class discovered by reflection and for each type in the list, an instance of the class is created using Activator.CreateInstance().
What is reflection?
In .NET an Assembly is a compiled output of code that contains .dll
and .exe
files and by using System.Reflection.Assembly
we can load and inspect our solution files programmatically
Combining dynamic discovery via reflection with builder pattern we achieved not only flexible but also an extensible architecture.
We have used reflection and dependency injection to load filters dynamically without hard coding.
Conclusion
Using this shifting mindset we can create robust and scalable system but scanning every assembly can lead to slowness therefore we should be specific for best practices.
Top comments (0)