Статьи

Шаблон стратегии пересматривается с весны

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