Example of a spring strategy form – DZone

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

What is a strategic pattern?

A 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 incorporate different strategies without changing the core logic of your application.

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

Step 1: Implementation of the strategy

Imagine yourself as a dark wizard seeking to master the power of the 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) at runtime.

Let’s start with our strategic interface:

public interface CurseStrategy   

    String useCurse();

	String curseName();

In the next step we need to implement all the 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: Insert Curses as a list

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

But first, let’s create the basics: the wizard interface.

public interface Wizard   
	String castCurse(String name); 

And we can insert 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 it is also created if the requested curse does not exist.

public class UnsupportedCurseException extends RuntimeException   

And we can confirm that casting a curse works:

@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 canUse method instead curseName. This will come back boolean and allows us to use more complex filtering such as:

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();  
	  

Advantages: Easy to implement.

Against: It goes through the loop every time, which can lead to slower execution time and increased processing load.

Step 3: Insert the strategies as a map

We can easily address the drawbacks from the previous section. Spring allows us to inject a folder 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 a key to Mapso the strategy names are the same as the bean names like cruciatusCurseStrategy. This dependency on Spring’s internal bean names can cause problems if Spring’s code or our class names change without notice.

Let’s see if we are still able to cast 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"));  
	  

Advantages: No loops.

Against: Dependency on bean names, which makes the code less maintainable and more error-prone if names are changed or refactored.

Step 4: Insert a list and convert to a map

The disadvantages of map injection can be easily removed if we inject a List and convert it to a 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 return to use curseName instead of Spring bean names for Map keys (strategy names).

Step 5: @Autowire in the interface

Spring supports automatic wiring into methods. A simple example of automatic wiring into methods is via setter injection. This feature allows us to use @Autowired in default interface method so we can register each CurseStrategy in Wizard interface without the need to implement a registration method in each strategy implementation.

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

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

This is 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 CurseStrategy interfaces by adding method s @Autowired comment:

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

At the moment of injecting addiction, we register our curse in Wizard.

Advantages: No loops and no reliance on internal Spring bean names.

Against: No flaws, pure dark magic.

Conclusion

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

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

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *