Strategy Pattern Revisited With Spring
This blog post wants to show another approach how to implement the Strategy Pattern with dependency injection. As DI framework, I choose Spring framework
From Wikipedia
Firstly, let's have a look how the Strategy Pattern is implemented in the classic way.
As starting point, we have a HeroController
that should add a hero in HeroRepository
depends on which repository was chosen by the user.
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.web.bind.annotation.ModelAttribute;
5import org.springframework.web.bind.annotation.PostMapping;
6
7@Controller
8public class HeroControllerClassicWay {
9
10 @PostMapping("/hero/new")
11 public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
12 HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
13 heroRepository.addHero(newHeroModel.getHero());
14 return "redirect:/hero";
15 }
16
17 private HeroRepository findHeroRepository(String repositoryName) {
18 if (repositoryName.equals("Unique")) {
19 return new UniqueHeroRepository();
20 }
21
22 if(repositoryName.equals(("Duplicate")){
23 return new DuplicateHeroRepository();
24 }
25
26 throw new IllegalArgumentException(String.format("Find no repository for given repository name \[%s\]", repositoryName));
27 }
28}
1package com.github.sparsick.springbootexample.hero.universum;
2
3import java.util.Collection;
4import java.util.HashSet;
5import java.util.Set;
6
7import org.springframework.stereotype.Repository;
8
9@Repository
10public class UniqueHeroRepository implements HeroRepository {
11
12 private Set<Hero> heroes = new HashSet<>();
13
14 @Override
15 public String getName() {
16 return "Unique";
17 }
18
19 @Override
20 public void addHero(Hero hero) {
21 heroes.add(hero);
22 }
23
24 @Override
25 public Collection<Hero> allHeros() {
26 return new HashSet<>(heroes);
27 }
28
29}
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Repository;
4
5import java.util.ArrayList;
6import java.util.Collection;
7import java.util.List;
8
9@Repository
10public class DuplicateHeroRepository implements HeroRepository {
11
12 private List<Hero> heroes = new ArrayList<>();
13
14 @Override
15 public void addHero(Hero hero) {
16 heroes.add(hero);
17 }
18
19 @Override
20 public Collection<Hero> allHeros() {
21 return List.copyOf(heroes);
22 }
23
24 @Override
25 public String getName() {
26 return "Duplicate";
27 }
28}
This implementation has some pitfalls. The creation of the repository implementations aren't managed by the Spring Context (it breaks the dependency injection / inverse of control). This will be painful as soon as you want to expand the repository implementation with further feature that need to inject other classes (for example, counting the usage of this class with MeterRegistry
).
1package com.github.sparsick.springbootexample.hero.universum;
2
3import java.util.Collection;
4import java.util.HashSet;
5import java.util.Set;
6
7import io.micrometer.core.instrument.Counter;
8import io.micrometer.core.instrument.MeterRegistry;
9import org.springframework.stereotype.Repository;
10
11@Repository
12public class UniqueHeroRepository implements HeroRepository {
13
14 private Set<Hero> heroes = new HashSet<>();
15 private Counter addCounter;
16
17 public UniqueHeroRepository(MeterRegistry meterRegistry) {
18 addCounter = meterRegistry.counter("hero.repository.unique");
19 }
20
21 @Override
22 public String getName() {
23 return "Unique";
24 }
25
26 @Override
27 public void addHero(Hero hero) {
28 addCounter.increment();
29 heroes.add(hero);
30 }
31
32 @Override
33 public Collection<Hero> allHeros() {
34 return new HashSet<>(heroes);
35 }
36
37}
It breaks also the separation of concern. When I want to test the controller class, I have no possibility to mock the repository interface easily. So the first idea is to put the creation of repository implementation to the Spring context. The repository implementation are annotated with @Repository
annotation. So Spring's component scan find them.
The next question how to inject them into the controller class. Here, a Spring feature can help. I define a list of HeroRepository
in the controller. This list has to be filled during the creation of the controller instance.
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.web.bind.annotation.ModelAttribute;
5import org.springframework.web.bind.annotation.PostMapping;
6
7import java.util.List;
8
9@Controller
10public class HeroControllerRefactoringStep1 {
11
12 private List<HeroRepository> heroRepositories;
13
14 public HeroControllerRefactoringStep1(List<HeroRepository> heroRepositories) {
15 this.heroRepositories = heroRepositories;
16 }
17
18 @PostMapping("/hero/new")
19 public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
20 HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
21 heroRepository.addHero(newHeroModel.getHero());
22 return "redirect:/hero";
23 }
24
25 private HeroRepository findHeroRepository(String repositoryName) {
26 return heroRepositories.stream()
27 .filter(heroRepository -> heroRepository.getName().equals(repositoryName))
28 .findFirst()
29 .orElseThrow(()-> new IllegalArgumentException(String.format("Find no repository for given repository name \[%s\]", repositoryName)));
30
31 }
32}
Spring searches in its context for all implementation of the interface HeroRepostiory
and put them all to the list. One disadvantage has this solution, every adding a hero browses the list of HeroRepository
to find the right implementation. This can be optimized by creating a map in the controller constructor that has the repository name as key and the corresponded implementation as value.
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.web.bind.annotation.ModelAttribute;
5import org.springframework.web.bind.annotation.PostMapping;
6
7import java.util.HashMap;
8import java.util.List;
9import java.util.Map;
10
11@Controller
12public class HeroControllerRefactoringStep2 {
13
14 private Map<String, HeroRepository> heroRepositories;
15
16 public HeroControllerRefactoringStep2(List<HeroRepository> heroRepositories) {
17 this.heroRepositories = heroRepositoryStrategies(heroRepositories);
18 }
19
20 private Map<String, HeroRepository> heroRepositoryStrategies(List<HeroRepository> heroRepositories){
21 Map<String, HeroRepository> heroRepositoryStrategies = new HashMap<>();
22 heroRepositories.forEach(heroRepository -> heroRepositoryStrategies.put(heroRepository.getName(), heroRepository));
23 return heroRepositoryStrategies;
24 }
25
26 @PostMapping("/hero/new")
27 public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
28 HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
29 heroRepository.addHero(newHeroModel.getHero());
30 return "redirect:/hero";
31 }
32
33 private HeroRepository findHeroRepository(String repositoryName) {
34 HeroRepository heroRepository = heroRepositories.get(repositoryName);
35 if(heroRepository != null) {
36 return heroRepository;
37 }
38 throw new IllegalArgumentException(String.format("Find no repository for given repository name \[%s\]", repositoryName));
39 }
40}
The final question is what if other classes in the application need the possibility to choose a repository implementation during the runtime. I could copy and paste the private method in each class that have this need or I move the creation of the map to the Spring Context and inject the Map to each class.
1package com.github.sparsick.springbootexample.hero;
2
3import com.github.sparsick.springbootexample.hero.universum.HeroRepository;
4import org.springframework.boot.SpringApplication;
5import org.springframework.boot.autoconfigure.SpringBootApplication;
6import org.springframework.context.annotation.Bean;
7
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11
12@SpringBootApplication
13public class HeroApplicationRefactoringStep3 {
14
15 public static void main(String\[\] args) {
16 SpringApplication.run(HeroApplication.class, args);
17 }
18
19 @Bean
20 Map<String, HeroRepository> heroRepositoryStrategy(List<HeroRepository> heroRepositories){
21 Map<String, HeroRepository> heroRepositoryStrategy = new HashMap<>();
22 heroRepositories.forEach(heroRepository -> heroRepositoryStrategy.put(heroRepository.getName(), heroRepository));
23 return heroRepositoryStrategy;
24 }
25}
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.ui.Model;
5import org.springframework.web.bind.annotation.ModelAttribute;
6import org.springframework.web.bind.annotation.PostMapping;
7
8import java.util.Map;
9
10@Controller
11public class HeroControllerRefactoringStep3 {
12
13 private Map<String, HeroRepository> heroRepositoryStrategy;
14
15 public HeroControllerRefactoringStep3(Map<String, HeroRepository> heroRepositoryStrategy) {
16 this.heroRepositoryStrategy = heroRepositoryStrategy;
17 }
18
19 @PostMapping("/hero/new")
20 public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
21 HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository());
22 heroRepository.addHero(newHeroModel.getHero());
23 return "redirect:/hero";
24 }
25
26 private HeroRepository findHeroRepository(String repositoryName) {
27 return heroRepositoryStrategy.get(repositoryName);
28 }
29
30}
This solution is a little bit ugly, because it isn't obvious that the Strategy Pattern is used. So the next refactoring step is moving the map of hero repositories to an own component class. Therefore, the bean definition heroRepositoryStrategy
in the application configuration can be removed.
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Component;
4
5import java.util.Collection;
6import java.util.HashMap;
7import java.util.Map;
8import java.util.Set;
9
10@Component
11public class HeroRepositoryStrategy {
12
13 private Map<String, HeroRepository> heroRepositoryStrategies;
14
15 public HeroRepositoryStrategy(Set<HeroRepository> heroRepositories) {
16 heroRepositoryStrategies = createStrategies(heroRepositories);
17 }
18
19 HeroRepository findHeroRepository(String repositoryName) {
20 return heroRepositoryStrategies.get(repositoryName);
21 }
22
23 Set<String> findAllHeroRepositoryStrategyNames () {
24 return heroRepositoryStrategies.keySet();
25 }
26
27 Collection<HeroRepository> findAllHeroRepositories(){
28 return heroRepositoryStrategies.values();
29 }
30
31
32 private Map<String, HeroRepository> createStrategies(Set<HeroRepository> heroRepositories){
33 Map<String, HeroRepository> heroRepositoryStrategies = new HashMap<>();
34 heroRepositories.forEach(heroRepository -> heroRepositoryStrategies.put(heroRepository.getName(), heroRepository));
35 return heroRepositoryStrategies;
36 }
37
38}
1package com.github.sparsick.springbootexample.hero.universum;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.ui.Model;
5import org.springframework.web.bind.annotation.GetMapping;
6import org.springframework.web.bind.annotation.ModelAttribute;
7import org.springframework.web.bind.annotation.PostMapping;
8
9import java.net.Inet4Address;
10import java.net.UnknownHostException;
11import java.util.ArrayList;
12import java.util.List;
13import java.util.Map;
14
15@Controller
16public class HeroController {
17
18 private HeroRepositoryStrategy heroRepositoryStrategy;
19
20 public HeroController(HeroRepositoryStrategy heroRepositoryStrategy) {
21 this.heroRepositoryStrategy = heroRepositoryStrategy;
22 }
23
24 @PostMapping("/hero/new")
25 public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) {
26 HeroRepository heroRepository = heroRepositoryStrategy.findHeroRepository(newHeroModel.getRepository());
27 heroRepository.addHero(newHeroModel.getHero());
28 return "redirect:/hero";
29 }
30}
The whole sample is hosted on GitHub.