Home > Enterprise >  Strategy Pattern with Dependency Injection?
Strategy Pattern with Dependency Injection?

Time:07-02

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();
}
  • Related