Update 03/05/2018
Steve mentioned in our discussion he was working on a post to do with the Dependency Injection with multiple implementations interface. It is now availble for a read here.
Update 04/04/2018
After a review by Steve Gordon (thanks Steve!) he has pointed out that the ASP.NET Core IoC container can’t handle IMathOperator[]
as a depenency however it can handle IEnumerable<IMathOperator>
and not require the additional factory pattern. This was a hangover from how unity did it due to it’s limitations and how I had implemented the pattern previously. Sorry ASP.NET Core!
The implementation can now be simplified by registering the IMathOperator
implementations as below.
services.AddScoped<IMathOperator, AddOperator>();
The MathStrategy
gets an update to take an IEnumerable<IMathOperator>
instead of an array in it’s constructor.
public class MathStrategy : IMathStrategy
{
private readonly IEnumerable<IMathOperator> _operators;
public MathStrategy(IEnumerable<IMathOperator> operators)
{
_operators = operators;
}
public int Calculate(int a, int b, Operator op)
{
return _operators.FirstOrDefault(x => x.Operator == op)?.Calculate(a, b) ?? throw new ArgumentNullException(nameof(op));
}
}
The ConfigureServices
method is much cleaner now. This is what I was expecting in the first place so I am glad it is possible.
public void ConfigureServices(IServiceCollection services)
{
** snip not related registrations **
services.AddScoped<IMathStrategy, MathStrategy>();
services.AddScoped<IMathOperator, AddOperator>();
services.AddScoped<IMathOperator, SubtractOperator>();
services.AddScoped<IMathOperator, MultipleOperator>();
services.AddScoped<IMathOperator, DivideOperator>();
}
The Github repository has been updated with the revised implementation.
The original post below has been left for reference.
Using design patterns in software development is very important. They help with code readability, maintainability and allow for applying SOLID principles to your code.
I have used the strategy pattern a number of times over the years for various tasks and the main part that I enjoy, once setup correctly, is that the main class does not need to be changed when adding more functionality into the processing.
To implement the strategy pattern cleanly you need to harness the power of using Dependency Injection and not rely on the concrete implementations.
After reading Strategy Pattern Implementations there are many ways of implementing the pattern but none of the methods James discuses harnesses the power of DI.
What is the strategy pattern?
The basic premise is the calling code has a single point of entry determined by user input or system specification. Based on the selection specified, in my example this will be an enum
value, it will locate a single processor which corresponds to that processor which will execute.
How do I implement this?
Working with the basic arithmatic operators, which James uses, as an example you will need the following:
IMathOperator
interface which defines the operator type and the calculation each implementation will execute.IMathStrategy
interface which the calling code will consume. This way the consuming code does not care about the implementation of the concrete classes and has a level of abstraction to allow for single responsibility.Operator
enum to define the action.
public enum Operator
{
Add,
Substract
}
public interface IMathOperator
{
Operator Operator { get; }
int Calculate(int a, int b);
}
public interface IMathStrategy
{
int Calculate(int a, int b, Operator op);
}
The implementation of the strategy requires to have the implementations of IMathOperator
injected into it’s constructor through DI.
public class MathStrategy : IMathStrategy
{
private readonly IMathOperator[] _operators;
public MathStrategy(IMathOperator[] operators)
{
_operators = operators;
}
public int Calculate(int a, int b, Operator op)
{
return _operators.FirstOrDefault(x => x.Operator == op)?.Calculate(a, b) ?? throw new ArgumentNullException(nameof(op));
}
}
Once they have been injected in when the Calculate
method is called the first processor which corresponds to the operator specified is located and executed.
With IoC containers, such as Unity, you can register all the concrete implementations of IMathOperator
as named registrations and then when an array of IMathOperator
are requested it injects all of the implementations. However this is not possible in the basic IoC in ASP.NET Core.
ASP.NET Core Restrictions
The IoC container which ships with ASP.NET Core is relatively basic, it allows for registration of implementatons with dependency trees however does not allow for the registration of the same interface with different implementations as Unity would and use them with a requirement of IMathOperator[]
.
Implementing in ASP.NET Core
It is possible to implement the above in ASP.NET Core with some juggling but you will require the use of a factory pattern at the point the dependencies are registered. As the strategy requires all of the IMathOperator
implementations the factory pattern has to get them all together ready for consumption.
public interface IMathStrategyFactory
{
IMathOperator[] Create();
}
The implementation of this factory relies on another function of the IoC container which is that you do not have to map interface to concrete implementation when you register the type with the IServiceCollection
. This factory implementation relies on the concrete implementations.
public class MathStrategyFactory : IMathStrategyFactory
{
private readonly AddOperator _addOperator;
private readonly SubtractOperator _subtractOperator;
public MathStrategyFactory(
AddOperator addOperator,
SubtractOperator subtractOperator)
{
_addOperator = addOperator;
_subtractOperator = subtractOperator;
}
public IMathOperator[] Create() => new IMathOperator[] { _addOperator, _subtractOperator };
}
Once the concrete implementations have been registered eg.
services.AddScoped<AddOperator>();
The factory has to be registered and then the use of it to allow for resolving the IMathOperator[]
for the strategy to be successful.
services.AddScoped<IMathStrategyFactory, MathStrategyFactory>();
services.AddScoped<IMathOperator[]>(provider =>
{
var factory = (IMathStrategyFactory)provider.GetService(typeof(IMathStrategyFactory));
return factory.Create();
});
How to consume the strategy?
Once all the registrations are in place the strategy needs to be consumed.
public class HomeController : ControllerBase
{
private readonly IMathStrategy _mathStrategy;
public HomeController(IMathStrategy mathStrategy)
{
_mathStrategy = mathStrategy;
}
public IActionResult Index()
{
int a = 10;
int b = 5;
int result = _mathStrategy.Calculate(a, b, Operator.Add);
return Content(result.ToString());
}
}
As you can see from the above, the HomeController
does not care about how the strategy pattern is implemented nor how the calculations are found and executed, it only cares about calling it with the values and specifying the action to run on them. This allows for easy unit testing as you can test each implementation separately and it means the HomeController
has only one interface to mock.
Open for extension
I mentioned at the beginning this method allows for applying the SOLID principles. The Single Responsibility Principle is applied to all concrete implementations as they have a single function to handle, the strategy is dealing with finding the appropriate implementation and calling it and the Controller calls the method and returns the result not caring about the processing.
But what about the Open/Closed principle? Well if we were to include a new operator, multiplication for example, we whould have to extend the enum, create a new concrete implementation and register it and that would be it.
Unfortunately as we are not using an IoC container such as Unity then we need to modify the factory which is a slight violation but I think one we can live with for the power of the functionality added.
Conclusion
We’ve looked at implementing the Strategy pattern harnessing the power of DI in ASP.NET Core to keep the consuming processing code SOLID.
A full working example can be found on Github.
Any questions/comments then please contact me on Twitter @WestDiscGolf