1

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.

RangerRick
  • 266
  • 4
  • 17
  • I have reviewed [this post](https://stackoverflow.com/questions/22701412/dependency-injection-and-the-strategy-pattern) that discusses possible solutions to this problem. However, I'm not a fan of any of the options proposed. They seem equally (if not more so) cumbersome as the enum solution I proposed above. My reasoning for this conclusion is that it relies heavily on *convention* rather than type safety. If a new developer onboards to the team, they may not understand these abstract conventions. I generally prefer to design things in a type-safe way, that does not rely on humans' memory. – RangerRick Jul 01 '22 at 15:52
  • How do you register a **method** to an interface? You can only map specific **classess** to interfaces. Your code above doesn't make much sense, IMHO. – MakePeaceGreatAgain Jul 01 '22 at 16:16
  • What about making your interface generic, so it would be `IMakeCoffee`, `IMakeCoffee`, etc? Could inject all of them into a single service that returns one based on type and you would have type safety and no need to use an enum. – Valuator Jul 01 '22 at 16:17
  • @MakePeaceGreatAgain, The `MakeLatte`, `MakeEspresso`, and `MakeCappuccino` are not methods. They are classes that implement the `IMakeCoffee` interface. Is suppose their names are a little confusing. This was just an example. I'm not actually working on a coffee making app lol – RangerRick Jul 01 '22 at 16:42
  • @Valuator, I like the way you're thinking, but doing it this way would mean that my `Barista` class would need to ask for *every* strategy in it's constructor like: `public Barista(IMakeCoffee espresso, IMakeCoffee latte) {}` and that isn't very dynamic if I decide to add new Coffee Making strategies... – RangerRick Jul 01 '22 at 16:44
  • 3
    Dependency injection is for configuring your application's behaviour, not a replacement for business logic. – Haukinger Jul 01 '22 at 16:46
  • @Haukinger, could you elaborate a little on your point? Am I thinking about Dependency Injection incorrectly? – RangerRick Jul 01 '22 at 16:48
  • 1
    You could bind `IMakeCoffee` to a factory method that returns such or such implementation based on parameterization (IIRC Ninject supports this), but then the more complex the rules get, the more apparent it becomes that such rules have no business in DI/IoC configurations. Injecting an abstract factory also leaves it in your _composition root_, so I wouldn't consider it (abstract factory absolutely _does_ have legitimate uses, just not as a strategy selector IMHO). OTOH having each strategy know when it's applicable leaves these rules where they belong, on the _business logic_ side of things. – Mathieu Guindon Jul 01 '22 at 18:03
  • 1
    *"Am I thinking about Dependency Injection incorrectly?"* Yes. The idea is to supply **one** service that can satisfy the specified interface. Granted, you *can* supply a different implementation for different circumstances, however it isn't convenient to "switch dynamically" between multiple different implementors. Therefore, your question is fundamentally misguided, IMHO. Use a factory to return the desired implementor, given an enum. If only ONE of EACH type of server is needed, the factory could internally cache those implementing instances, in a dictionary. Factory can be DI, if needed. – ToolmakerSteve Jul 01 '22 at 22:56

3 Answers3

1

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 NotSupportedException("No applicable strategy was found for the specified context");
}

return strategy.MakeCoffee();
Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • I'd switch `FirstOrDefault()` and the useless null check for one `Single()` . – AgentFire Jul 01 '22 at 16:26
  • @AgentFire What if there are more than one, or even no, strategies for a given coffee type? – Haukinger Jul 01 '22 at 16:43
  • @Haukinger it's perfectly defensible if you're okay with letting .Single throw; I prefer controlling the stack trace and trowing my own with a more meaningful message in such a case, but if the containing method is small enough (and well-named, e.g. `FindApplicableStrategy`) that an InvalidOperationException thrown from Enumerable.Single still clearly reads as "no applicable strategy was found" then it's likely fine also. – Mathieu Guindon Jul 01 '22 at 17:04
  • @Haukinger that would mean you have a hole in your logic. Better get to fixin. – AgentFire Jul 01 '22 at 17:12
0

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.

PotterJam
  • 39
  • 1
  • 7
0

You could create some sort of a strategy selector:

public interface IStrategySelector<TKey, TStrategy>
{
    TStrategy Select(TKey key);
}

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();
}
AgentFire
  • 8,944
  • 8
  • 43
  • 90