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.
Join the DZone community and get the full member experience.
Join For FreeIn 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:
public interface CurseStrategy {
String useCurse();
String curseName();
}
In the next step, we need to implement all Unforgivable curses:
@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.
public interface Wizard {
String castCurse(String name);
}
And we can inject our curses (strategies) into the Wizard and filter the desired one.
@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.
public class UnsupportedCurseException extends RuntimeException {
}
And we can verify that curse casting is working:
@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:
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.
@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:
@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:
@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:
public interface Wizard {
String castCurse(String name);
void registerCurse(String curseName, CurseStrategy curse)
}
This is the Wizard
implementation:
@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:
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.
Opinions expressed by DZone contributors are their own.
Comments