В этом блоге мы хотим показать другой подход к реализации шаблона стратегии с помощью внедрения зависимостей. В качестве 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; @Controller public 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; @Repository public 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; @Repository public 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; @Repository public 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; @Controller public 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; @Controller public 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; @SpringBootApplication public 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; @Controller public 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; @Component public 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; @Controller public 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, являются их собственными. |