Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Strategy Pattern Revisited With Spring

DZone 's Guide to

Strategy Pattern Revisited With Spring

Here's the Strategy Pattern revisited with the Spring Framework.

· Java Zone ·
Free Resource

This blog post demonstrates another approach on how to implement the strategy pattern with dependency injection. As for my DI framework, I choose the Spring Framework:

Image courtesy of Wikipedia

Firstly, let's have a look at how the Strategy Pattern is implemented in a classic way. As a starting point, we have a HeroController that should add a hero in HeroRepository depends on which repository was chosen by the user.

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

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&gt; heroes = new HashSet<&gt;();

    @Override
    public String getName() {
        return "Unique";
    }

    @Override
    public void addHero(Hero hero) {
        heroes.add(hero);
    }

    @Override
    public Collection<Hero&gt; allHeros() {
        return new HashSet<&gt;(heroes);
    }

}

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&gt; heroes = new ArrayList<&gt;();

    @Override
    public void addHero(Hero hero) {
        heroes.add(hero);
    }

    @Override
    public Collection<Hero&gt; allHeros() {
        return List.copyOf(heroes);
    }

    @Override
    public String getName() {
        return "Duplicate";
    }
}


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 a further feature that needs to inject other classes (for example, counting the usage of this class with MeterRegistry).


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&gt; heroes = new HashSet<&gt;();
    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&gt; allHeros() {
        return new HashSet<&gt;(heroes);
    }

}


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 in the Spring context. The repository implementation is annotated with @Repository annotation. So, Spring's component scan find them.

The next question of 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.

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&gt; heroRepositories;

    public HeroControllerRefactoringStep1(List<HeroRepository&gt; 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 -&gt; heroRepository.getName().equals(repositoryName))
                .findFirst()
                .orElseThrow(()-&gt; new IllegalArgumentException(String.format("Find no repository for given repository name [%s]", repositoryName)));

    }
}


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.


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&gt; heroRepositories;

    public HeroControllerRefactoringStep2(List<HeroRepository&gt; heroRepositories) {
        this.heroRepositories = heroRepositoryStrategies(heroRepositories);
    }

    private Map<String, HeroRepository&gt; heroRepositoryStrategies(List<HeroRepository&gt; heroRepositories){
        Map<String, HeroRepository&gt; heroRepositoryStrategies = new HashMap<&gt;();
        heroRepositories.forEach(heroRepository -&gt; 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));
    }
}


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 has this need, or I move the creation of the map to the Spring Context and inject the Map to each class.

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 HeroApplication {

public static void main(String[] args) {
SpringApplication.run(HeroApplication.class, args);
}

@Bean
    Map<String, HeroRepository&gt; heroRepositoryStrategy(List<HeroRepository&gt; heroRepositories){
        Map<String, HeroRepository&gt; heroRepositoryStrategy = new HashMap<&gt;();
        heroRepositories.forEach(heroRepository -&gt; heroRepositoryStrategy.put(heroRepository.getName(), heroRepository));
        return heroRepositoryStrategy;
    }
}

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 HeroController {

    private Map<String, HeroRepository&gt; heroRepositoryStrategy;

    public HeroController(Map<String, HeroRepository&gt; 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);
    }

}


The whole sample is hosted on GitHub.


Topics:
java ,spring ,strategy pattern ,tutorial ,spring framework ,repository

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}