Is it possible to combine a book from 1994 with one of the most popular frameworks of today? Of course it is. This is an example of how you take full advantage of both.

It's been over 15 years since the book Design Patterns: Elements of Reusable Object-Oriented Software by "The Gang of Four" was published. The book is one of the most influential books written on the subject of software engineering. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides were not the first (and certainly not the last) to write about how using common patterns lead to better code but the book is so popular that it has been published in 38 prints so far.

What is a Strategy pattern?

The design pattern Strategy is a way of grouping implementations so they can be interchanged in runtime. Wikipedia defines it as:

... the strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable...

There are many situations where a Strategy pattern can be used, for instance when you need different logic for different countries or as is often the case when you want a desktop application to have different behavior than a tablet or a smartphone. We are going to look at an example where we change the implementation based on the current user's data.

What is Spring Framework?

Spring Framework is an open source framework that makes life easier for most developers and was originally created as a counter point to the bloated J2EE architecture that was prevalent during the early part of this millenium. Spring provides dependency injection along with a flexible way of creating the infrastructure for most applications. It provides support for handling transactions, databases, proxying, messaging, MVC, etc. Spring also has multiple sub-projects that provide support for additional integration.

Setting up our Factory

We will look at the relevant parts of the implementation but a link to the full source code can be found at the bottom of the page for those who want to look into this in more detail.

With only a little configuration setup Spring will scan the classpath for any classes annotated with @Service, @Repository or @Component. We want to extend that by adding our own annotation type. Let's take a look at what that class looks like.

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Strategy {
 Class type();
 Profile[] profiles() default {};
}
code lang="java">
 
p>Adding the code>@Component

annotation to the Strategy class makes Spring pick it up automatically when it scans for annotations. Profile is an enum that defines FREE, LIMITED and PREMIUM. Each Strategy can have 0 or multiple profiles specified. How profiles are used will become clearer once we look at a snippet from the factory. The factory is needed to keep a cache of the annoatated beans, searching for annotated classes every time a strategy is used would not offer very good performance. The factory's job is to provide an easy way for us to get the correct strategy.

public T> T getStrategy(ClassT> strategyType, Profile currentProfile) {
 
 ListObject> strategyBeans = annotatedTypes.get(strategyType);
 Assert.notEmpty(strategyBeans, "No strategies found of type '"+ strategyType.getName()+"', are the strategies marked with @Strategy?");
 
 Object profileStrategy = findStrategyMatchingProfile(strategyBeans, currentProfile);
 if (profileStrategy == null) {
 throw new RuntimeException("No strategy found for type '"+ strategyType +"'");
 }
 return (T)profileStrategy;
}
 
private Object findStrategyMatchingProfile(ListObject> strategyBeans, Profile currentProfile) {
 
 Object defaultStrategy = null;
 for (Object bean : strategyBeans) {
 Strategy strategyAnnotation = strategyCache.get(bean.getClass());
 if(currentProfile != null) {
 //Only iterate the profiles if a profile has been selected
 for (Profile profile : strategyAnnotation.profiles()) {
 if (profile == currentProfile) {
 return bean;
 }
 }
 }
 
 if (isDefault(strategyAnnotation)) {
 defaultStrategy = bean;
 // if we found the default and no profile is specified we can return
 if(currentProfile == null) {
 return defaultStrategy;
 }
 }
 }
 return defaultStrategy;
}

The factory will read and cache all instances of objects with our Strategy annotation. The factory offers one public method, getStrategy, which will try to find a strategy that matches the profile specified by the caller. If no matching profile is found it will return the default strategy. A default strategy is defined by not having a profile at all which we will see an example of later.
This is our foundation for our Strategy pattern.

Implementing a Strategy

A simple example for strategy is navigation. We will show different navigations to different users depending on their profile. We start by creating a simple interface.

public interface NavigationStrategy {
 void createNavigation(ModelAndView modelAndView);
}

As you see from this interface it is nothing but a plain interface and not tied to anything strategy specific. There is no limitation or restriction on this interface. After creating our interface what we need is to create the implementation for LIMITED and PREMIUM.

@Strategy(type=NavigationStrategy, profiles={Profile.LIMITED, Profile.PREMIUM})
public class LimitedAndPremiumNavigationStrategy implements NavigationStrategy {
 public void createNavigation(ModelAndView modelAndView) {
 // Create the navigation and add it to the modelAndView
 }
}

By adding the profiles=... on our annotation we are matching it against the users profile. If no profiles are added it will become the default Strategy.

@Strategy(type=NavigationStrategy.class)
public class FreeNavigationStrategy implements NavigationStrategy {
 @Override
 public void createNavigation(ModelAndView modelAndView) {
 // Create the navigation and add it to the modelAndView
 }
}

Using the Strategy

Using our strategy is as easy as autowiring the factory in any class that is managed by the Spring context.

@Autowire
private StrategyFactory strategyFactory;
 
public void someMethodInSomeServiceOrController(ModelAndView modelAndView) {
 NavigationStrategy navigationStrategy = strategyFactory.getStrategy(NavigationStrategy.class, currentUserProfile);
 navigationStrategy.createNavigation(modelAndView);
}

We call the getStrategy method with our interface and what is returned is the correct implementation. What that implementation is we don't need to worry about, all we do is call the method createNavigation.

What have we gained?

We no longer have to have nested if-statements to check for different user profiles. It is already handled and we can focus on the business logic for each profile. Also, by breaking up the implementations for our navigation in different classes we can very easily write tests for each of them with a lot less mocking.

Conclusion

As you can see it is not only possible to combine a book from 1994 and the ubiquitous Spring Framework but it is a very good thing to do so. Spring Framework gives developers a lot of tools to make our jobs easier but that shouldn't stop us from using the time tested skills of knowledge and experience.

A simple runnable web app example is available on GitHub