LoginSignup
11
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-28

はじめに

SpringBootでStrategy パターンを適用したサンプルコードを書いてみました。
Strategy パターンについては IT専科さんの解説記事 などを参照してください。
サンプルコードは GitHub に公開しています。

動作環境

  • Java 11
  • SpringBoot 2.3.0

題材

大学の図書館を題材にしています。ユーザは本の貸出、返却ができます。またユーザは役職によって借りられる本の数が変化することとします。今回、Strategyにあてはめるのはユーザの役職によって変化する処理の部分です。サンプルコードでは教授、院生、学部生の場合を実装しています。
Strategyを利用する(呼び出す)側はFactoryに依頼をして目的にあったStrategyを取得します。

クラス図

クラス図

インターフェイスの定義

インターフェイスでは、各役職に依存した処理を定義しています。ユーザごとに借りれる本の上限が決まるわけではなく、役職ごとに決定するので上限冊数を返却するメソッドも定義します。また、役職を識別するEnumである PositionName を返却するメソッドも定義します。
borrowBookreturnBook で引数としている UserBook は単なるPOJOです。詳しくは GitHub を参照してください。

BorrowerStrategy.java
public interface BorrowerStrategy {

    int getMaxBorrowNum();

    User borrowBook(User user, Book book);

    User returnBook(User user, Book book);

    PositionName getPositionName();
}
PositionName.java
public enum PositionName {
    PROFESSOR,
    GRADUATE_STUDENT,
    COLLEGE_STUDENT
}

Strategyの実装

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

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;
    }
}
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を注入することができます

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

実装の確認

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

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

}

結果

👍👍👍
テスト結果

Strategy パターンを適用するメリット

  • 同じようなswitch文が乱立しない
  • パターンの増減にも対応しやすい

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

さいごに

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

ソースコード

GitHub

参考サイト

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7