Search code examples
javaphpdesign-patternsstrategy-patternservice-locator

Avoid Service locator in strategy design pattern


Take a look on that pseudocode

class A,B,C implements StrategyInterface
{
    private dep;

    constructor(Dep dep) {
        this->dep = dep;    
    }
}

class StrategyResolver
{
    private locator;

    constructor(ServiceLocator locator) {
        this->locator = locator;
    }
    
    public function resolve(data): StrategyInterface
    {
        if ( xxx ) {
            return locator->get(A);
        } else if ( yyy ) {
            return locator->get(B);
        }
        return locator->get(C);
    }
}

As service locator is considered as anti pattern, how to avoid it in this case? A,B,C can have various dependencies, thats why I would like to instantiate it using all benefits of dependency injections. I could albo inject A,B,C as dependencies of StrategyResolver, but what if I have there like 10 strategies. The StrategyResolver dependency list is then too long.


Solution

  • If you are using Spring you can inject all known beans of a certain type automatically:

    private final List<StrategyInterface> strategies;
    
    public StrategyResolver(List<StrategyInterface> strategies) {
        this.strategies = strategies;
    }
    

    If this is the only constructor Spring will have no trouble injecting all the beans implementing the StrategyInterface. Of course those beans need to be known to Spring using for example the @Named annotation on the class:

    @javax.inject.Named
    public class A implements StrategyInterface {
    

    BTW why not just name the interface 'Strategy'? The 'Interface' suffix does not really add anything.

    EDIT: to determine which Strategy applies you can add a method to the Strategy interface:

    @javax.inject.Named
    public class A implements StrategyInterface {
    
        @Override
        public boolean appliesTo(String data) {
            // just an example
            return "xxx".equals(data);
        }
    

    In the StrategyResolver you would have code like this:

    public StrategyInterface resolve(String data) {
        for(StrategyInterface strategy : strategies) {
            if (strategy.appliesTo(data)) {
                return strategy;
            }
        }
        throw new IllegalArgumentException("No strategy applies to " + data);
    }
    

    Or in more recent style:

    public StrategyInterface resolve(String data) {
        return strategies.stream().filter(s -> s.appliesTo(data)).findAny()
            .orElseThrow(() -> new IllegalArgumentException("No strategy applies to " + data));
    }
    

    One gotcha here is that to avoid the exception above you must ensure that a strategy always applies. You could create a 'DefaultStrategy' which always applies and is added as the last strategy in the list, e.g:

    private final List<StrategyInterface> strategies;
    
    public StrategyResolver(List<StrategyInterface> strategies, DefaultStrategy defaultStrategy) {
        this.strategies = new ArrayList<>(strategies);    
        this.strategies.add(defaultStrategy);
    }