В этом блоге мы хотим показать другой подход к реализации шаблона стратегии с помощью внедрения зависимостей. В качестве DI Framework я выбираю Spring Framework

Во-первых, давайте посмотрим, как шаблон стратегии реализован классическим способом.
В качестве отправной точки у нас есть HeroController который должен добавить героя в HeroRepository зависимости от того, какой репозиторий был выбран пользователем.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;@Controllerpublic class HeroControllerClassicWay { @PostMapping("/hero/new") public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) { HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository()); heroRepository.addHero(newHeroModel.getHero()); return "redirect:/hero"; } private HeroRepository findHeroRepository(String repositoryName) { if (repositoryName.equals("Unique")) { return new UniqueHeroRepository(); } if(repositoryName.equals(("Duplicate")){ return new DuplicateHeroRepository(); } throw new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName)); }} |
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package com.github.sparsick.springbootexample.hero.universum;import java.util.Collection;import java.util.HashSet;import java.util.Set;import org.springframework.stereotype.Repository;@Repositorypublic class UniqueHeroRepository implements HeroRepository { private Set<Hero> heroes = new HashSet<>(); @Override public String getName() { return "Unique"; } @Override public void addHero(Hero hero) { heroes.add(hero); } @Override public Collection<Hero> allHeros() { return new HashSet<>(heroes); }} |
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Repository;import java.util.ArrayList;import java.util.Collection;import java.util.List;@Repositorypublic class DuplicateHeroRepository implements HeroRepository { private List<Hero> heroes = new ArrayList<>(); @Override public void addHero(Hero hero) { heroes.add(hero); } @Override public Collection<Hero> allHeros() { return List.copyOf(heroes); } @Override public String getName() { return "Duplicate"; }} |
Эта реализация имеет некоторые подводные камни. Создание реализаций репозитория не управляется Spring Context (это нарушает внедрение зависимостей / обратное управление). Это будет болезненно, как только вы захотите расширить реализацию репозитория с помощью дополнительной функции, которая должна внедрять другие классы (например, подсчет использования этого класса с MeterRegistry ).
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package com.github.sparsick.springbootexample.hero.universum;import java.util.Collection;import java.util.HashSet;import java.util.Set;import io.micrometer.core.instrument.Counter;import io.micrometer.core.instrument.MeterRegistry;import org.springframework.stereotype.Repository;@Repositorypublic class UniqueHeroRepository implements HeroRepository { private Set<Hero> heroes = new HashSet<>(); private Counter addCounter; public UniqueHeroRepository(MeterRegistry meterRegistry) { addCounter = meterRegistry.counter("hero.repository.unique"); } @Override public String getName() { return "Unique"; } @Override public void addHero(Hero hero) { addCounter.increment(); heroes.add(hero); } @Override public Collection<Hero> allHeros() { return new HashSet<>(heroes); }} |
Это также нарушает разделение интересов. Когда я хочу протестировать класс контроллера, у меня нет возможности легко смоделировать интерфейс репозитория. Поэтому первая идея заключается в том, чтобы поместить реализацию репозитория в контекст Spring. Реализация репозитория @Repository аннотацией @Repository . Таким образом, сканирование компонентов Spring находит их.
Следующий вопрос, как внедрить их в класс контроллера. Здесь может помочь функция Spring. Я определяю список HeroRepository в контроллере. Этот список должен быть заполнен при создании экземпляра контроллера.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;import java.util.List;@Controllerpublic class HeroControllerRefactoringStep1 { private List<HeroRepository> heroRepositories; public HeroControllerRefactoringStep1(List<HeroRepository> heroRepositories) { this.heroRepositories = heroRepositories; } @PostMapping("/hero/new") public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) { HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository()); heroRepository.addHero(newHeroModel.getHero()); return "redirect:/hero"; } private HeroRepository findHeroRepository(String repositoryName) { return heroRepositories.stream() .filter(heroRepository -> heroRepository.getName().equals(repositoryName)) .findFirst() .orElseThrow(()-> new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName))); }} |
Spring ищет в своем контексте все реализации интерфейса HeroRepostiory и помещает их всех в список. У этого решения есть один недостаток: каждое добавление героя просматривает список HeroRepository чтобы найти правильную реализацию. Это можно оптимизировать, создав карту в конструкторе контроллера, в которой имя хранилища является ключом, а соответствующая реализация — значением.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;import java.util.HashMap;import java.util.List;import java.util.Map;@Controllerpublic class HeroControllerRefactoringStep2 { private Map<String, HeroRepository> heroRepositories; public HeroControllerRefactoringStep2(List<HeroRepository> heroRepositories) { this.heroRepositories = heroRepositoryStrategies(heroRepositories); } private Map<String, HeroRepository> heroRepositoryStrategies(List<HeroRepository> heroRepositories){ Map<String, HeroRepository> heroRepositoryStrategies = new HashMap<>(); heroRepositories.forEach(heroRepository -> heroRepositoryStrategies.put(heroRepository.getName(), heroRepository)); return heroRepositoryStrategies; } @PostMapping("/hero/new") public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) { HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository()); heroRepository.addHero(newHeroModel.getHero()); return "redirect:/hero"; } private HeroRepository findHeroRepository(String repositoryName) { HeroRepository heroRepository = heroRepositories.get(repositoryName); if(heroRepository != null) { return heroRepository; } throw new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName)); }} |
Последний вопрос заключается в том, что если другим классам в приложении требуется возможность выбрать реализацию репозитория во время выполнения. Я мог бы скопировать и вставить приватный метод в каждый класс, в котором есть такая необходимость, или перенести создание карты в контекст Spring и вставить карту в каждый класс.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package com.github.sparsick.springbootexample.hero;import com.github.sparsick.springbootexample.hero.universum.HeroRepository;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import java.util.HashMap;import java.util.List;import java.util.Map;@SpringBootApplicationpublic class HeroApplicationRefactoringStep3 { public static void main(String[] args) { SpringApplication.run(HeroApplication.class, args); } @Bean Map<String, HeroRepository> heroRepositoryStrategy(List<HeroRepository> heroRepositories){ Map<String, HeroRepository> heroRepositoryStrategy = new HashMap<>(); heroRepositories.forEach(heroRepository -> heroRepositoryStrategy.put(heroRepository.getName(), heroRepository)); return heroRepositoryStrategy; }} |
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;import java.util.Map;@Controllerpublic class HeroControllerRefactoringStep3 { private Map<String, HeroRepository> heroRepositoryStrategy; public HeroControllerRefactoringStep3(Map<String, HeroRepository> heroRepositoryStrategy) { this.heroRepositoryStrategy = heroRepositoryStrategy; } @PostMapping("/hero/new") public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) { HeroRepository heroRepository = findHeroRepository(newHeroModel.getRepository()); heroRepository.addHero(newHeroModel.getHero()); return "redirect:/hero"; } private HeroRepository findHeroRepository(String repositoryName) { return heroRepositoryStrategy.get(repositoryName); }} |
Это решение немного некрасиво, потому что не очевидно, что используется шаблон стратегии. Поэтому следующим шагом рефакторинга является перемещение карты репозиториев героев в собственный класс компонентов. Следовательно, определение компонента heroRepositoryStrategy в конфигурации приложения может быть удалено.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Component;import java.util.Collection;import java.util.HashMap;import java.util.Map;import java.util.Set;@Componentpublic class HeroRepositoryStrategy { private Map<String, HeroRepository> heroRepositoryStrategies; public HeroRepositoryStrategy(Set<HeroRepository> heroRepositories) { heroRepositoryStrategies = createStrategies(heroRepositories); } HeroRepository findHeroRepository(String repositoryName) { return heroRepositoryStrategies.get(repositoryName); } Set<String> findAllHeroRepositoryStrategyNames () { return heroRepositoryStrategies.keySet(); } Collection<HeroRepository> findAllHeroRepositories(){ return heroRepositoryStrategies.values(); } private Map<String, HeroRepository> createStrategies(Set<HeroRepository> heroRepositories){ Map<String, HeroRepository> heroRepositoryStrategies = new HashMap<>(); heroRepositories.forEach(heroRepository -> heroRepositoryStrategies.put(heroRepository.getName(), heroRepository)); return heroRepositoryStrategies; }} |
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package com.github.sparsick.springbootexample.hero.universum;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;import java.net.Inet4Address;import java.net.UnknownHostException;import java.util.ArrayList;import java.util.List;import java.util.Map;@Controllerpublic class HeroController { private HeroRepositoryStrategy heroRepositoryStrategy; public HeroController(HeroRepositoryStrategy heroRepositoryStrategy) { this.heroRepositoryStrategy = heroRepositoryStrategy; } @PostMapping("/hero/new") public String addNewHero(@ModelAttribute("newHero") NewHeroModel newHeroModel) { HeroRepository heroRepository = heroRepositoryStrategy.findHeroRepository(newHeroModel.getRepository()); heroRepository.addHero(newHeroModel.getHero()); return "redirect:/hero"; }} |
Весь образец размещен на GitHub .
|
Опубликовано на Java Code Geeks с разрешения Сандры Парсик, партнера нашей программы JCG. Посмотрите оригинальную статью здесь: Стратегия паттерна вновь с весны Мнения, высказанные участниками Java Code Geeks, являются их собственными. |