1

In my current problem domain, a number of problems seem to lend themselves quite well to the strategy pattern. I have a common, high-level process - let's say it's a sales process - and regardless of the sales channel I use, sellProduct needs to happen... It's just a bit different from channel to channel.

So let's say I want to implement a strategy pattern per-channel.

What I'd really like to know, is whether there's a robust, common way that people have seen this implemented. Typically what I've seen in the past is some kind of lookup, perhaps from a DB or config file, based on the channel type, that instantiates a particular class and then executes SalesStrategy.execute() on that implementation. That said, I'm not sure if storing fully-qualified class names in a DB or config file is really the best we can do here.

Does anyone else have other thoughts? I'm using Java, so interested in what sort of common configuration-based solutions people have used through something like Guice or Spring.

  • <rant>This java "execute()" patterns sometimes get in my nerves. Here, have a Story About Noums and Verbs.</rant>. – Machado Mar 2 '18 at 16:54
  • While not entirely on topic, that did give me a good chuckle. Thanks. – f1dave Mar 7 '18 at 1:21
2

No, your database/config files should not mention class names.

Instead, you should include the channel in your model, as it is a relevant part of your domain. This doesn't have to be fancy - a simply enum may be sufficient. If your strategy depends on many parameters, you can group them into a SalesStrategyConfiguration.

The actual strategy is then instantiated by a factory, which evaluates the provided parameters and instantiates the correct strategy. This decision is based purely on domain-relevant concepts, so there is no need for any specific framework.

Where a framework may well come in handy is the translation of the config data to the model (i.e. creating the SalesStrategyConfiguration). But this is a very general task, in no way related to strategy instantiation.

  • Sounds similar to the basic enum->strategy instance map that I'm currently using then. It just felt a bit ugly - especially if the various strategy implementations required different dependencies, you end up dragging in a lot of stuff to construct them all (which is where my thoughts about Guice were coming from) – f1dave Mar 1 '18 at 1:23
  • The strategy factory's client shouldn't really require many dependencies - those should be abstracted. Note that you may use an injection framework, and if the framework is set up with config files, those may of course reference class names (they're basically source files). But your application's config files should not depend on implementation details. Maybe I misunderstood your question? – doubleYou Mar 1 '18 at 22:54
0

I had this code inspired by https://www.captechconsulting.com/blogs/combining-strategy-pattern-and-spring as the entry point to my utility library, use the following code snippets at your convenience:

Strategy.java

package ...

@Documented
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Strategy {

    Class<?> type();

    String[] profiles() default {};
}

StrategyFactory.java

package ...

public class StrategyFactory {

    private static final Logger LOG = Logger.getLogger( StrategyFactory.class );

    private Map<Class<?>, Strategy> strategiesCache = new HashMap<Class<?>, Strategy>();

    private String[] packages;

    public void init() {
        if (this.packages != null) {
            Set<Class<?>> annotatedClasses = new HashSet<Class<?>>();
            for (String pack : this.packages) {
                Reflections reflections = new Reflections( pack );
                annotatedClasses.addAll( reflections.getTypesAnnotatedWith( Strategy.class ) );
            }
            this.sanityCheck( annotatedClasses );
        }
    }

    public <T> T getStrategy(Class<T> strategyClass) {
        return this.getStrategy( strategyClass, null );
    }

    @SuppressWarnings("unchecked")
    public <T> T getStrategy(Class<T> strategyClass, String currentProfile) {
        Class<T> clazz = (Class<T>) this.findStrategyMatchingProfile( strategyClass, currentProfile );
        if (clazz == null) {
            throw new StrategyNotFoundException( String.format( "No strategies found of type '%s', are the strategies marked with @Strategy?", strategyClass.getName() ) );
        }
        try {
            return (T) clazz.newInstance();
        } catch (Exception e) {
            throw ExceptionUtils.rethrowAs( e, StrategyException.class );
        }
    }

    /**
     * Checks to make sure there is only one strategy of each type(Interface) annotated for each profile Will throw an exception on startup if multiple strategies are mapped to the same profile.
     * @param annotatedClasses a list of classes
     */
    private void sanityCheck(Set<Class<?>> annotatedClasses) {
        Set<String> usedStrategies = new HashSet<String>();
        for (Class<?> annotatedClass : annotatedClasses) {
            Strategy strategyAnnotation = AnnotationUtils.findAnnotation( annotatedClass, Strategy.class );
            if (!strategyAnnotation.type().isAssignableFrom( annotatedClass )) {
                throw new StrategyProfileViolationException( String.format( "'%s' should be assignable from '%s'", strategyAnnotation.type(), annotatedClass ) );
            }
            this.strategiesCache.put( annotatedClass, strategyAnnotation );

            if (this.isDefault( strategyAnnotation )) {
                this.ifNotExistAdd( strategyAnnotation.type(), "default", usedStrategies );
            } else {
                for (String profile : strategyAnnotation.profiles()) {
                    this.ifNotExistAdd( strategyAnnotation.type(), profile, usedStrategies );
                }
            }
        }
    }

    private void ifNotExistAdd(Class<?> type, String profile, Set<String> usedStrategies) {
        String key = this.createKey( type, profile );
        if (usedStrategies.contains( key )) {
            throw new StrategyProfileViolationException( String.format( "There can only be a single strategy for each type, found multiple for type '%s' and profile '%s'", type, profile ) );
        }
        usedStrategies.add( key );
    }

    private String createKey(Class<?> type, String profile) {
        return String.format( "%s_%s", type, profile ).toLowerCase();
    }

    private boolean isDefault(Strategy strategyAnnotation) {
        return (strategyAnnotation.profiles().length == 0);
    }

    private Class<?> findStrategyMatchingProfile(Class<?> strategyClass, String currentProfile) {
        for (Map.Entry<Class<?>, Strategy> strategyCacheEntry : this.strategiesCache.entrySet()) {
            Strategy strategyCacheEntryValue = strategyCacheEntry.getValue();
            if (strategyCacheEntryValue.type().equals( strategyClass )) {
                if (currentProfile != null) {
                    for (String profile : strategyCacheEntryValue.profiles()) {
                        if (currentProfile.equals( profile )) {
                            Class<?> result = strategyCacheEntry.getKey();
                            if (LOG.isDebugEnabled()) {
                                LOG.debug( String.format( "Found strategy [strategy=%s, profile=%s, strategyImpl=%s]", strategyClass, currentProfile, result ) );
                            }
                            return result;
                        }
                    }
                } else if (this.isDefault( strategyCacheEntryValue )) {
                    Class<?> defaultClass = strategyCacheEntry.getKey();
                    if (LOG.isDebugEnabled()) {
                        LOG.debug( String.format( "Found default strategy [strategy=%s, profile=%s, strategyImpl=%s]", strategyClass, currentProfile, defaultClass ) );
                    }
                    return defaultClass;
                }
            }
        }
        return null;
    }

    public void setPackages(String[] packages) {
        this.packages = packages;
    }
}

StrategyException.java

package ...

public class StrategyException extends RuntimeException {
...
}

StrategyNotFoundException.java

package ...

public class StrategyNotFoundException extends StrategyException {
...
}

StrategyProfileViolationException.java

package ...

public class StrategyProfileViolationException extends StrategyException {
...
}

Usage without Spring:

NavigationStrategy.java

package com.asimio.core.test.strategy.strategies.navigation;

public interface NavigationStrategy {

    public String naviateTo();
}

FreeNavigationStrategy.java

package com.asimio.core.test.strategy.strategies.navigation;

@Strategy(type = NavigationStrategy.class)
public class FreeNavigationStrategy implements NavigationStrategy {

    public String naviateTo() {
        return "free";
    }
}

LimitedPremiumNavigationStrategy.java

package com.asimio.core.test.strategy.strategies.navigation;

@Strategy(type = NavigationStrategy.class, profiles = { "limited", "premium" })
public class LimitedPremiumNavigationStrategy implements NavigationStrategy {

    public String naviateTo() {
        return "limited+premium";
    }
}

Then

...
StrategyFactory factory = new StrategyFactory();
factory.setPackages( new String[] { "com.asimio.core.test.strategy.strategies.navigation" } );
this.factory.init();

NavigationStrategy ns = this.factory.getStrategy( NavigationStrategy.class );
String result = ns.naviateTo();
Assert.assertThat( "free", Matchers.is( result ) );
...
Or
...
String result = factory.getStrategy( NavigationStrategy.class, "limited" ).naviateTo();
Assert.assertThat( "limited+premium", Matchers.is( result ) );
...
  • The code is nice and all, but without any explanation why it should be written like this it just enables cargo cult programming. If you provided some commentary this would be a much better answer. – Mael Mar 2 '18 at 7:12
  • Thank you for the link. In terms of your example, I'd prefer to avoid reflection but the link has given me a few ideas around annotations. – f1dave Mar 7 '18 at 11:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.