Problem description
Consider the following implementation of the strategy pattern:
// this is the strategy definition
public interface ICalculator
{
int ComputeResult(int a, int b);
}
// this is an implementation of the strategy
public sealed class SumCalculator: ICalculator
{
public int ComputeResult(int a, int b) => a + b;
}
// this is another implementation of the strategy
public sealed class SubtractionCalculator: ICalculator
{
public int ComputeResult(int a, int b) => a - b;
}
Let's suppose we need to write some client code for the ICalculator
service. The client code is given the following input data:
- an integer number, via the variable
a
- another integer number, via the variable
b
- contextual information used to decide which strategy needs to be used. Let's suppose to have an enum named
TaskType
whose possible values areSum
andSubtract
.
From a functional perspective, the client code should do something like this:
int a = GetFirstOperand();
int b = GetSecondOperand();
TaskType taskType = GetTaskType();
ICalculator calculator = null;
switch(taskType)
{
case TaskType.Sum:
calculator = new SumCalculator();
break;
case TaskType.Subtract:
calculator = new SubtractionCalculator();
break;
default:
throw new NotSupportedException($"Task type {taskType} is not supported");
}
int result = calculator.ComputeResult(a,b);
Console.Writeline($"The result is: {result}");
Consider now a codebase using dependency injection and delegating object creation and lifetime management to a DI container. In this case, the client code of the ICalculator
service can't directly take the responsibility of creating objects.
What I'm trying to find is, basically, an elegant and effective way to solve this problem.
What I usually do in this scenario
This is what I usually do to solve this problem. I call this design pattern the composite design pattern, but I'm quite sure this is not exactly the pattern named composite design pattern in the gang of four book.
First of all, a reshape of the ICalculator
interface is needed (more on this later):
public interface ICalculator
{
int ComputeResult(int a, int b, TaskType taskType);
bool CanHandleTask(TaskType taskType);
}
The existing interface implementations need to be changed:
public sealed class SumCalculator: ICalculator
{
public int ComputeResult(int a, int b, TaskType taskType)
{
if (!this.CanHandleTask(taskType))
{
throw new InvalidOperationException($"{nameof(SumCalculator)} cannot handle task {taskType}");
}
return a + b;
}
public bool CanHandleTask(TaskType taskType) => taskType == TaskType.Sum;
}
public sealed class SubtractionCalculator: ICalculator
{
public int ComputeResult(int a, int b, TaskType taskType)
{
if (!this.CanHandleTask(taskType))
{
throw new InvalidOperationException($"{nameof(SubtractionCalculator)} cannot handle task {taskType}");
}
return a - b;
}
public bool CanHandleTask(TaskType taskType) => taskType == TaskType.Subtract;
}
A third implementation of the ICalculator
interface needs to be written. I call this object the composite object:
public sealed class CompositeCalculator: ICalculator
{
private readonly IEnumerable<ICalculator> _calculators;
public CompositeCalculator(IEnumerable<ICalculator> calculators)
{
_calculators = calculators ?? throw new ArgumentNullException(nameof(calculators));
}
public int ComputeResult(int a, int b, TaskType taskType)
{
if (!this.CanHandleTask(taskType))
{
throw new InvalidOperationException($"{nameof(CompositeCalculator)} cannot handle task {taskType}");
}
var handler = _calculators.First(x => x.CanHandleTask(taskType));
return handler.ComputeResult(a, b, taskType);
}
public bool CanHandleTask(TaskType taskType) => _calculators.Any(x => x.CanHandleTask(taskType));
}
This is the client code of ICalculator
:
// this class encapsulates the client code of ICalculator
public sealed class AnotherService
{
private readonly ICalculator _calculator;
public AnotherService(ICalculator calculator)
{
_calculator = calculator ?? throw new ArgumentNullException(nameof(calculator));
}
public void DoSomething()
{
// code omitted for brevity
int a = ...;
int b = ...;
TaskType taskType = ...;
var result = _calculator.ComputeResult(a, b, taskType);
Console.Writeline($"The result is {result}");
}
}
Finally, here is the registration of the ICalculator
interface in the DI container:
services.AddSingleton<SumCalculator>();
services.AddSingleton<SubtractionCalculator>();
services.AddSingleton<ICalculator>(sp =>
{
var calculators = new List<ICalculator>
{
sp.GetRequiredService<SumCalculator>(),
sp.GetRequiredService<SubtractionCalculator>()
};
return new CompositeCalculator(calculators);
});
This pattern works, but I don't like the fact that the ICalculator
interface needs to be modified in order to introduce the CanHandleTask
method and the extraneous parameter taskType
to the ComputeResult
method.
The original definition of the ICalculator
interface (see the Problem description above) seems to be a more natural definition for a service able to compute a result using two integer numbers as input to the computation.
An alternative solution
An alternative solution to this problem is introducing a factory object for the ICalculator
interface. This is somewhat similar to the IHttpClientFactory
interface introduced in .NET core.
This way we can keep the original definition for the ICalculator
interface:
public interface ICalculator
{
int ComputeResult(int a, int b);
}
We need to introduce a factory object for ICalculator
instances:
public interface ICalculatorFactory
{
ICalculator CreateCalculator(TaskType taskType);
}
These are the implementations of the ICalculator
interface (no more need for the composite object):
public sealed class SumCalculator: ICalculator
{
public int ComputeResult(int a, int b) => a + b;
}
public sealed class SubtractionCalculator: ICalculator
{
public int ComputeResult(int a, int b) => a - b;
}
This is the new version of the client code:
// this class encapsulates the client code of ICalculator
public sealed class AnotherService
{
private readonly ICalculatorFactory _calculatorFactory;
public AnotherService(ICalculatorFactory calculatorFactory)
{
_calculatorFactory = calculatorFactory ?? throw new ArgumentNullException(nameof(calculatorFactory));
}
public void DoSomething()
{
// code omitted for brevity
int a = ...;
int b = ...;
TaskType taskType = ...;
var calculator = _calculatorFactory.CreateCalculator(taskType);
var result = calculator.ComputeResult(a, b);
Console.Writeline($"The result is {result}");
}
}
The concrete implementation for the ICalculatorFactory
interface delegates the object creation to the DI container and is defined inside the composition root (because it depends on the DI container directly):
public sealed class ServiceProviderCalculatorFactory: ICalculatorFactory
{
private readonly IServiceProvider _serviceProvider;
public ServiceProviderCalculatorFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public ICalculator CreateCalculator(TaskType taskType)
{
switch(taskType)
{
case TaskType.Sum:
return _serviceProvider.GetRequiredService<SumCalculator>();
case TaskType.Subtract:
return _serviceProvider.GetRequiredService<SubtractionCalculator>();
default:
throw new NotSupportedException($"Task type {taskType} is not supported");
}
}
}
Finally, here is the service registration on the DI container:
services.AddSingleton<SumCalculator>();
services.AddSingleton<SubtractionCalculator>();
services.AddSingleton<ICalculatorFactory, ServiceProviderCalculatorFactory>();
The main advantange of this solution is avoiding all of the CanHandle
ceremony of the composite pattern described above.
Question
Is there a better or canonical way to resolve this problem ?
Aucun commentaire:
Enregistrer un commentaire