SpringBootでStrategyパターンを適用する

# はじめに
SpringBootでStrategy パターンを適用したサンプルコードを書いてみました。
Strategy パターンについては [IT専科さんの解説記事](http://www.itsenka.com/contents/development/designpattern/strategy.html) などを参照してください。
サンプルコードは [GitHub](https://github.com/Yukiho-YOSHIEDA/spring-strategy-pattern) に公開しています。

# 動作環境
* Java 11
* SpringBoot 2.3.0

# 題材
大学の図書館を題材にしています。ユーザは本の貸出、返却ができます。またユーザは役職によって借りられる本の数が変化することとします。今回、Strategyにあてはめるのはユーザの役職によって変化する処理の部分です。サンプルコードでは教授、院生、学部生の場合を実装しています。
Strategyを利用する（呼び出す）側はFactoryに依頼をして目的にあったStrategyを取得します。
## クラス図
![クラス図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543358/827a329e-d783-f306-49d3-488b81548e31.png)

# インターフェイスの定義
インターフェイスでは、各役職に依存した処理を定義しています。ユーザごとに借りれる本の上限が決まるわけではなく、役職ごとに決定するので上限冊数を返却するメソッドも定義します。また、役職を識別するEnumである `PositionName` を返却するメソッドも定義します。
`borrowBook` と `returnBook` で引数としている `User` と `Book` は単なるPOJOです。詳しくは [GitHub](https://github.com/Yukiho-YOSHIEDA/spring-strategy-pattern) を参照してください。

```java:BorrowerStrategy.java
public interface BorrowerStrategy {

    int getMaxBorrowNum();

    User borrowBook(User user, Book book);

    User returnBook(User user, Book book);

    PositionName getPositionName();
}
```

```java:PositionName.java
public enum PositionName {
    PROFESSOR,
    GRADUATE_STUDENT,
    COLLEGE_STUDENT
}
```

# Strategyの実装
先程定義したインターフェイスを実装します。教授と院生の場合を例として挙げます。今回は実装が同じメソッドが目立ちますが、権限によって借りられる本の種類が制限されたりすると更にありがたみを感じることでしょう。
後述しますが、SpringによるDIを利用したいので `@Component` アノテーションをクラスに付与します。

```java:ProfessorStrategy.java
@Component
public class ProfessorStrategy implements BorrowerStrategy {

    @Override
    public int getMaxBorrowNum() {
        return 50;
    }

    @Override
    public User borrowBook(User user, Book book) {
        if (user.getCurrentNum() >= getMaxBorrowNum()) {
            throw new CannotBorrowBookException();
        }
        final var books = new ArrayList<>(user.getBooks());
        books.add(book);
        return new User(user.getName(), user.getPositionName(), books);
    }

    @Override
    public User returnBook(User user, Book book) {
        if (user.getCurrentNum() == 0 || !user.getBooks().contains(book)) {
            throw new CannotReturnBookException(book.getName());
        }
        final var books = new ArrayList<>(user.getBooks());
        books.remove(book);
        return new User(user.getName(), user.getPositionName(), books);
    }


    @Override
    public PositionName getPositionName() {
        return PositionName.PROFESSOR;
    }
}
```

```java:GraduateStudentStrategy.java
@Component
public class GraduateStudentStrategy implements BorrowerStrategy {
    @Override
    public int getMaxBorrowNum() {
        return 30;
    }

    @Override
    public User borrowBook(User user, Book book) {
        if (user.getCurrentNum() >= getMaxBorrowNum()) {
            throw new CannotBorrowBookException();
        }
        final var books = new ArrayList<>(user.getBooks());
        books.add(book);
        return new User(user.getName(), user.getPositionName(), books);
    }

    @Override
    public User returnBook(User user, Book book) {
        if (user.getCurrentNum() == 0 || !user.getBooks().contains(book)) {
            throw new CannotReturnBookException(book.getName());
        }
        final var books = new ArrayList<>(user.getBooks());
        books.remove(book);
        return new User(user.getName(), user.getPositionName(), books);
    }


    @Override
    public PositionName getPositionName() {
        return PositionName.GRADUATE_STUDENT;
    }
}
```

# Factoryの実装
Factoryは主にサービスから呼ばれることを想定しているので、 `@Component` アノテーションをクラスに付与します。このクラスは `Set<BorrowStrategy> brrorwerStrategySet` をAutowireしているのですが、これを記述することで **BrrorwerStrategyを継承したBeanのSetを注入することができます** 。

```java:BorrowerStrategyFactory.java
@Component
public class BorrowerStrategyFactory {

    private Map<PositionName, BorrowerStrategy> borrowerStrategyMap;

    @Autowired
    public BorrowerStrategyFactory(Set<BorrowerStrategy> borrowerStrategySet) {
        createStrategy(borrowerStrategySet);
    }

    public BorrowerStrategy findStrategy(PositionName positionName) {
        return borrowerStrategyMap.get(positionName);
    }

    private void createStrategy(Set<BorrowerStrategy> borrowerStrategySet) {
        borrowerStrategyMap = borrowerStrategySet.stream()
                .collect(Collectors.toMap(BorrowerStrategy::getPositionName, borrowerStrategy -> borrowerStrategy));
    }
}
```

# 実装の確認
今回の実装をユニットテストで確認していきます。ここでは一部をご紹介します。

```java:BorrowerStrategyFactoryTest.java
@SpringBootTest
public class BorrowerStrategyFactoryTest {

    static final String PROFESSOR = "Professor";
    static final String GRADUATE_STUDENT = "GraduateStudent";
    static final String COLLEGE_STUDENT = "CollegeStudent";

    @Autowired
    BorrowerStrategyFactory borrowerStrategyFactory;

    @Test
    void canGetAllStrategy() {
        final var professorStrategy = borrowerStrategyFactory.findStrategy(PositionName.PROFESSOR);
        assertThat(professorStrategy).isInstanceOf(ProfessorStrategy.class);
        final var graduateStudentStrategy = borrowerStrategyFactory.findStrategy(PositionName.GRADUATE_STUDENT);
        assertThat(graduateStudentStrategy).isInstanceOf(GraduateStudentStrategy.class);
        final var collegeStudentStrategy = borrowerStrategyFactory.findStrategy(PositionName.COLLEGE_STUDENT);
        assertThat(collegeStudentStrategy).isInstanceOf(CollegeStudentStrategy.class);
    }

    @ParameterizedTest
    @MethodSource
    void successBorrowBook(User user) {
        final var borrowerStrategy = borrowerStrategyFactory.findStrategy(user.getPositionName());
        user = borrowerStrategy.borrowBook(user, new Book("Book1"));
        assertThat(user.getCurrentNum()).isEqualTo(1);
    }

    static Stream<Arguments> successBorrowBook() {
        return Stream.of(
            Arguments.arguments(new User(PROFESSOR, PositionName.PROFESSOR)),
            Arguments.arguments(new User(GRADUATE_STUDENT, PositionName.GRADUATE_STUDENT)),
            Arguments.arguments(new User(COLLEGE_STUDENT, PositionName.COLLEGE_STUDENT))
        );
    }

    // 中略

    @ParameterizedTest
    @MethodSource
    void failedReturnBookWhenNotFoundBook(User user) {
        final var borrowerStrategy = borrowerStrategyFactory.findStrategy(user.getPositionName());
        final var failedUser = borrowerStrategy.borrowBook(user, new Book("Book1"));
        assertThrows(CannotReturnBookException.class, () -> borrowerStrategy.returnBook(failedUser, new Book("Book2")));
        assertThat(failedUser.getCurrentNum()).isEqualTo(1);
    }

    static Stream<Arguments> failedReturnBookWhenNotFoundBook() {
        return Stream.of(
            Arguments.arguments(new User(PROFESSOR, PositionName.PROFESSOR)),
            Arguments.arguments(new User(GRADUATE_STUDENT, PositionName.GRADUATE_STUDENT)),
            Arguments.arguments(new User(COLLEGE_STUDENT, PositionName.COLLEGE_STUDENT))
        );
    }

}
```

## 結果
👍👍👍
![テスト結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543358/928a3935-0d11-9a6a-8919-700ce15a44e3.jpeg)

# Strategy パターンを適用するメリット
* 同じようなswitch文が乱立しない
* パターンの増減にも対応しやすい

2つ目のメリットに関しては、いらなくなったらそのクラスを削除することで対応できます。今回のケースでは「期間限定で市民が本を借りれるようにしたい」といった要望に答えることができます。

# さいごに
Strategy パターンを適用するとswitch文の多用で煩雑になりそうなケースが回避できる場合があります。Strategy パターンに似たデザインパターンであるState パターンもあるので、気になる方は調べてみてください。

# ソースコード
[GitHub](https://github.com/Yukiho-YOSHIEDA/spring-strategy-pattern)

# 参考サイト
* [Strategy Design Pattern with in Spring Boot application.](https://medium.com/@ravthiru/strategy-design-pattern-with-in-spring-boot-application-2ff5a7486cd8)
* [Strategy パターン - デザインパターン入門 - IT専科](http://www.itsenka.com/contents/development/designpattern/strategy.html)