Spring Strategy Pattern Example

In this tutorial, explore various Strategy pattern implementations in the Spring framework such as list injection, map injection, and method injection.

In this example, we’ll learn about the Strategy pattern in Spring. We’ll cover different ways to inject strategies, starting from a simple list-based approach to a more efficient map-based method. To illustrate the concept, we’ll use the three Unforgivable curses from the Harry Potter series — Avada Kedavra, Crucio, and Imperio.

What Is the Strategy Pattern?

The Strategy Pattern is a design principle that allows you to switch between different algorithms or behaviors at runtime. It helps make your code flexible and adaptable by allowing you to plug in different strategies without changing the core logic of your application.

This approach is useful in scenarios where you have different implementations for a specific task of functionality and want to make your system more adaptable to changes. It promotes a more modular code structure by separating the algorithmic details from the main logic of your application.

Step 1: Implementing Strategy

Picture yourself as a dark wizard who strives to master the power of Unforgivable curses with Spring. Our mission is to implement all three curses — Avada Kedavra, Crucio and Imperio. After that, we will switch between curses (strategies) in runtime.

Let’s start with our strategy interface:

Java

public interface CurseStrategy {
    String useCurse();
	String curseName();
}

In the next step, we need to implement all Unforgivable curses:

Java

@Component
public class CruciatusCurseStrategy implements CurseStrategy {  
	@Override  
	public String useCurse() {  
		return "Attack with Crucio!";  
	}  
	@Override  
	public String curseName() {  
		return "Crucio";  
	}  
}
@Component  
public class ImperiusCurseStrategy implements CurseStrategy {  
	@Override  
	public String useCurse() {  
		return "Attack with Imperio!";  
	}  
	@Override  
	public String curseName() {  
		return "Imperio";  
	}  
}
@Component  
public class KillingCurseStrategy implements CurseStrategy {  
	@Override  
	public String useCurse() {  
		return "Attack with Avada Kedavra!";  
	}  
	@Override  
	public String curseName() {  
		return "Avada Kedavra";  
	}  
}

Step 2: Inject Curses as List

Spring brings a touch of magic that allows us to inject multiple implementations of an interface as a List so we can use it to inject strategies and switch between them.

But let’s first create the foundation: Wizard interface.

Java

public interface Wizard {
	String castCurse(String name); 
}

And we can inject our curses (strategies) into the Wizard and filter the desired one.

Java
@Service public class DarkArtsWizard implements Wizard { private final List<CurseStrategy> curses; public DarkArtsListWizard(List<CurseStrategy> curses) { this.curses = curses; } @Override public String castCurse(String name) { return curses.stream() .filter(s -> name.equals(s.curseName())) .findFirst() .orElseThrow(UnsupportedCurseException::new) .useCurse(); } }

UnsupportedCurseException is also created if the requested curse does not exist.

Java

public class UnsupportedCurseException extends RuntimeException {
}

And we can verify that curse casting is working:

Java
@SpringBootTest class DarkArtsWizardTest { @Autowired private DarkArtsWizard wizard; @Test public void castCurseCrucio() { assertEquals("Attack with Crucio!", wizard.castCurse("Crucio")); } @Test public void castCurseImperio() { assertEquals("Attack with Imperio!", wizard.castCurse("Imperio")); } @Test public void castCurseAvadaKedavra() { assertEquals("Attack with Avada Kedavra!", wizard.castCurse("Avada Kedavra")); } @Test public void castCurseExpelliarmus() { assertThrows(UnsupportedCurseException.class, () -> wizard.castCurse("Abrakadabra")); } }

Another popular approach is to define the canUse method instead of curseName. This will return boolean and allows us to use more complex filtering like:

Java
public interface CurseStrategy { String useCurse(); boolean canUse(String name, String wizardType); } @Component public class CruciatusCurseStrategy implements CurseStrategy { @Override public String useCurse() { return "Attack with Crucio!"; } @Override public boolean canUse(String name, String wizardType) { return "Crucio".equals(name) && "Dark".equals(wizardType); } } @Service public class DarkArtstWizard implements Wizard { private final List<CurseStrategy> curses; public DarkArtsListWizard(List<CurseStrategy> curses) { this.curses = curses; } @Override public String castCurse(String name) { return curses.stream() .filter(s -> s.canUse(name, "Dark"))) .findFirst() .orElseThrow(UnsupportedCurseException::new) .useCurse(); } }

Pros: Easy to implement.

Cons: Runs through a loop every time, which can lead to slower execution times and increased processing overhead.

Step 3: Inject Strategies as Map

We can easily address the cons from the previous section. Spring lets us inject a Map with bean names and instances. It simplifies the code and improves its efficiency.

Java
@Service public class DarkArtsWizard implements Wizard { private final Map<String, CurseStrategy> curses; public DarkArtsMapWizard(Map<String, CurseStrategy> curses) { this.curses = curses; } @Override public String castCurse(String name) { CurseStrategy curse = curses.get(name); if (curse == null) { throw new UnsupportedCurseException(); } return curse.useCurse(); } }

This approach has a downside: Spring injects the bean name as the key for the Map, so strategy names are the same as the bean names like cruciatusCurseStrategy. This dependency on Spring’s internal bean names might cause problems if Spring’s code or our class names change without notice.

Let’s check that we’re still capable of casting those curses:

Java
@SpringBootTest class DarkArtsWizardTest { @Autowired private DarkArtsWizard wizard; @Test public void castCurseCrucio() { assertEquals("Attack with Crucio!", wizard.castCurse("cruciatusCurseStrategy")); } @Test public void castCurseImperio() { assertEquals("Attack with Imperio!", wizard.castCurse("imperiusCurseStrategy")); } @Test public void castCurseAvadaKedavra() { assertEquals("Attack with Avada Kedavra!", wizard.castCurse("killingCurseStrategy")); } @Test public void castCurseExpelliarmus() { assertThrows(UnsupportedCurseException.class, () -> wizard.castCurse("Crucio")); } }

Pros: No loops.

Cons: Dependency on bean names, which makes the code less maintainable and more prone to errors if names are changed or refactored.

Step 4: Inject List and Convert to Map

Cons of Map injection can be easily eliminated if we inject List and convert it to Map:

Java
@Service public class DarkArtsWizard implements Wizard { private final Map<String, CurseStrategy> curses; public DarkArtsMapWizard(List<CurseStrategy> curses) { this.curses = curses.stream() .collect(Collectors.toMap(CurseStrategy::curseName, Function.identity())); } @Override public String castCurse(String name) { CurseStrategy curse = curses.get(name); if (curse == null) { throw new UnsupportedCurseException(); } return curse.useCurse(); } }

With this approach, we can move back to use curseName instead of Spring’s bean names for Map keys (strategy names).

Step 5: @Autowire in Interface

Spring supports autowiring into methods. The simple example of autowiring into methods is through setter injection. This feature allows us to use @Autowired in a default method of an interface so we can register each CurseStrategy in the Wizard interface without needing to implement a registration method in every strategy implementation.

Let’s update the Wizard interface by adding a registerCurse method:

Java

public interface Wizard {
	String castCurse(String name);  
	void registerCurse(String curseName, CurseStrategy curse)
}

This is the Wizard implementation:

Java
@Service public class DarkArtsWizard implements Wizard { private final Map<String, CurseStrategy> curses = new HashMap<>(); @Override public String castCurse(String name) { CurseStrategy curse = curses.get(name); if (curse == null) { throw new UnsupportedCurseException(); } return curse.useCurse(); } @Override public void registerCurse(String curseName, CurseStrategy curse) { curses.put(curseName, curse); } }

Now, let’s update the CurseStrategy interface by adding a method with the @Autowired annotation:

Java

public interface CurseStrategy {
	String useCurse();  
	String curseName();  
	@Autowired  
	default void registerMe(Wizard wizard) {  
		wizard.registerCurse(curseName(), this);  
	}  
}

At the moment of injecting dependencies, we register our curse into the Wizard.

Pros: No loops, and no reliance on inner Spring bean names.

Cons: No cons, pure dark magic.

Conclusion

In this article, we explored the Strategy pattern in the context of Spring. We assessed different strategy injection approaches and demonstrated an optimized solution using Spring’s capabilities.

The full source code for this article can be found on GitHub.

You may also like