はじめに
SpringBootでStrategy パターンを適用したサンプルコードを書いてみました。
Strategy パターンについては IT専科さんの解説記事 などを参照してください。
サンプルコードは GitHub に公開しています。
動作環境
- Java 11
- SpringBoot 2.3.0
題材
大学の図書館を題材にしています。ユーザは本の貸出、返却ができます。またユーザは役職によって借りられる本の数が変化することとします。今回、Strategyにあてはめるのはユーザの役職によって変化する処理の部分です。サンプルコードでは教授、院生、学部生の場合を実装しています。
Strategyを利用する(呼び出す)側はFactoryに依頼をして目的にあったStrategyを取得します。
クラス図
インターフェイスの定義
インターフェイスでは、各役職に依存した処理を定義しています。ユーザごとに借りれる本の上限が決まるわけではなく、役職ごとに決定するので上限冊数を返却するメソッドも定義します。また、役職を識別するEnumである PositionName
を返却するメソッドも定義します。
borrowBook
と returnBook
で引数としている User
と Book
は単なるPOJOです。詳しくは GitHub を参照してください。
public interface BorrowerStrategy {
int getMaxBorrowNum();
User borrowBook(User user, Book book);
User returnBook(User user, Book book);
PositionName getPositionName();
}
public enum PositionName {
PROFESSOR,
GRADUATE_STUDENT,
COLLEGE_STUDENT
}
Strategyの実装
先程定義したインターフェイスを実装します。教授と院生の場合を例として挙げます。今回は実装が同じメソッドが目立ちますが、権限によって借りられる本の種類が制限されたりすると更にありがたみを感じることでしょう。
後述しますが、SpringによるDIを利用したいので @Component
アノテーションをクラスに付与します。
@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;
}
}
@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を注入することができます 。
@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));
}
}
実装の確認
今回の実装をユニットテストで確認していきます。ここでは一部をご紹介します。
@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 パターンもあるので、気になる方は調べてみてください。