Is there an elegant way to deal with dependency injection for the strategy pattern?
For instance, I have my dependency injector that I register a few different strategies with, like so:
container.Register<IMakeCoffee, MakeLatte>();
container.Register<IMakeCoffee, MakeEspresso>();
container.Register<IMakeCoffee, MakeCappuccino>();
And then when I want to access one of these strategies, I inject it into my constructor like:
public Barista(IEnumerable<IMakeCoffee> coffeeStrategies) {}
But once I have that list of all the strategies, how can I say I want this specific strategy? What I'm doing now is forcing each instance of IMakeCoffee
to have a CoffeeType enum that I can key off of to determine what type the strategy is.
public Coffee MakeCoffee(CoffeeType coffeeType)
{
var strat = coffeeStrategies.FirstOrDefault(s => s.CoffeeType == coffeeType);
return strat.MakeCoffee();
}
Is there a more elegant way to uniquely identify a strategy? The enum business seems cumbersome.
For reference, I am using Prism for a Xamarin Forms app, but I felt this question was framework independent.
CodePudding user response:
Selecting a strategy with a "coffee type" enum makes sense, but the selector query is intrusive in the sense that it needs to be aware of various implementation details in order to make a selection, and nothing says the type of coffee will always ever be the only discriminator for any given strategy.
I'd keep the details encapsulated, and have the strategy interface expose a bool IsApplicable(CoffeeMakerStrategyContext context)
method that each strategy can implement differently as needed. The method could take the CoffeeType
enum as a parameter, but then the signature would need to change if a new strategy comes along but requires more data to make a decision; by instead passing some "strategy context" object, the signature doesn't need to change (similar to why we use EventArgs
for event parameters):
public interface IStrategy<TContext>
{
bool IsApplicable(TContext context);
}
public interface ICoffeeMakerStrategy : IStrategy<CoffeeMakerStrategyContext>
{
Coffee MakeCoffee();
}
The selector code becomes:
var context = new CoffeeMakerStrategyContext { CoffeeType = coffeeType };
var strategy = _strategies.FirstOrDefault(e => e.IsApplicable(context));
if (strategy is null)
{
throw new InvalidOperationException("No applicable strategy was found for the specified context");
}
return strategy.MakeCoffee();
CodePudding user response:
My first thought is that a lack of discriminated unions make this more frustrating on a type level. For the convenience and to get the try get pattern for free I'd be tempted to do something like:
Dictionary<CoffeeType, IMakeCoffee> m_CoffeeMakers;
public Barista(IEnumerable<IMakeCoffee> coffeeStrategies)
{
m_CoffeeMakers = coffeeStrategies.ToDictionary(x => x.HandledCoffeeType, x => x);
}
public Coffee MakeCoffee(CoffeeType coffeeType)
{
if (!m_CoffeeMakers.TryGet(coffeeType, out var coffeeMaker)
{
throw new InvalidOperationException($"unsupported coffee type {coffeeType}");
}
return coffeeMaker.MakeCoffee();
}
I thought you could perhaps use DI'd decorators, but it's a bit of a mess because the bottom of the chain would have to return null, making it more messy anyway.
CodePudding user response:
You could create some sort of a strategy selector:
public interface IStrategySelector<TEnumType, TStrategy> where TEnumType : Enum
{
TStrategy Select(TEnumType enum);
}
And then you could place your selection logic into the implementation:
public sealed class CoffeeStrategySelector : IStrategySelector<CoffeeType, IMakeCoffee>
{
public IMakeCoffee Select(CoffeeType coffeeType)
{
// Place your selection logic here.
}
}
You could require your IEnumerable<IMakeCoffee>
from the selector's constructor, or use any other technical way to implement your selection logic.
Your barista would be happy:
private readonly IStrategySelector<CoffeeType, IMakeCoffee> _coffeeSelector;
public Barista(IStrategySelector<CoffeeType, IMakeCoffee> coffeeSelector)
{
_coffeeSelector = coffeeSelector;
}
public Coffee MakeCoffee(CoffeeType coffeeType)
{
IMakeCoffee strat = _coffeeSelector.Select(coffeeType);
return strat.MakeCoffee();
}