영호

[OOP] EnumMap과 함수형 인터페이스로 커맨드 패턴 적용해보기 본문

OOP

[OOP] EnumMap과 함수형 인터페이스로 커맨드 패턴 적용해보기

0h0 2023. 3. 26. 01:10

우테코 5기 레벨1 체스미션을 진행하면서 커맨드 패턴을 적용한 과정을 정리해보겠습니다:)

왜 적용했나?

체스미션에는 다양한 명령어가 있다. 움직이는 MOVE, 게임을 시작하는 START, 게임 점수를 보여주는 STATUS등등이 있다. 1단계 컨트롤러를 구현하면서 시간이 없어서 명령어마다 분기문을 통해 수행되는 로직을 구현한 결과 매우 더러운 코드가 나왔다.

private void play(ChessGame chessGame) {
    List<String> userCommandInput = repeatBySupplier(inputView::requestUserCommandInGame);
    try {
        GameCommand command = GameCommand.from(userCommandInput);
    if (isExitCommand(command)) {
        chessGameService.saveChessGame(ChessGameSaveRequestDto.from(chessGame));
    }
    if (!chessGame.isPlayable()) {
        return;
    }
    if (userCommandInput.equals("status")) {
        Map<Side, Double> scores = chessGame.calculateScores();
        outputView.printGameScores(scores);
        outputView.printWinner(chessGame.calculateWinner());
    }
    //move
    try {
        Position sourcePosition = Position.of(inputs.get(SOURCE_FILE_INDEX), inputs.get(SOURCE_RANK_INDEX));
        Position targetPosition = Position.of(inputs.get(TARGET_FILE_INDEX), inputs.get(TARGET_RANK_INDEX));

        chessGame.move(sourcePosition, targetPosition);
    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

}

상당히 더러워서 읽기도 싫다. 그래서 명령어.execute()를 하면 명령어에 맞는 기능이 수행되도록 고치고 싶었다. 주변 크루들에게 이러한 고민을 말하니까 커맨드 패턴을 얘기하길래 적용해봤다.

EnumMap과 BiConsumer를 이용한 패턴 적용

EnumMap을 사용한 이유는, 현재 명령어는 GameCommand라는 Enum으로 관리하고 있기 때문에, 명령어 별로 동작을 수행하기 위해서 GameCommand를 Key로 가지는 EnumMap을 사용했다.

 

BiConsumer를 사용한 이유는, 게임 시작 이후 받을 수 있는 명령어는 현재 MOVE, END, STATUS가 있다. 3가지 명령어 모두 반환타입은 없고 parameter는 필요하다. 그 중 MOVE명령어는 2개의 parameter(ChessGame, 이동 경로 List)가 필요했기 때문에 BiConsumer를 사용했다.

private final Map<GameCommand, BiConsumer<ChessGame, List<String>>> commands;
public ChessController(InputView inputView, OutputView outputView, ChessGameService chessGameService) {
    this.inputView = inputView;
    this.outputView = outputView;
    this.chessGameService = chessGameService;
    this.commands = new EnumMap<>(GameCommand.class);

    commands.put(GameCommand.END, (chessGame, positions) -> endCommandExecute(chessGame));
    commands.put(GameCommand.STATUS, (chessGame, positions) -> statusCommandExecute(chessGame));
    commands.put(GameCommand.MOVE, (chessGame, positions) -> moveCommandExecute(chessGame, positions));
}

위 코드와 같이 EnumMap을 GameCommand, BiConsumer를 이용해 생성했다. 그리고 수행할 로직을 메서드로 만들고 람다를 이용해 명령어 별 수행 메서드를 저장했다.

 

아래 코드는 각각의 명령어가 수행할 로직을 메서드로 추출한 결과입니다.

private void endCommandExecute(ChessGame chessGame) {
    chessGameService.saveChessGame(ChessGameSaveRequestDto.from(chessGame));
    chessGame.end();
}

private void statusCommandExecute(ChessGame chessGame) {
    Map<Side, Double> scores = chessGame.calculateScores();
    outputView.printGameScores(scores);
    outputView.printWinner(chessGame.calculateWinner());
}

private void moveCommandExecute(ChessGame chessGame, List<String> commands) {
    String sourceText = commands.get(SOURCE_TEXT_INDEX);
    String targetText = commands.get(TARGET_TEXT_INDEX);

    chessGame.move(convertPosition(sourceText), convertPosition(targetText));
}

private Position convertPosition(String positionText) {
    List<String> positionTexts = Arrays.asList(positionText.split(POSITION_DELIMITER));
    return Position.of(positionTexts.get(FILE_INDEX), positionTexts.get(RANK_INDEX));
}

코드 비교

private void play(ChessGame chessGame) {
    List<String> userCommandInput = repeatBySupplier(inputView::requestUserCommandInGame);
    try {
        GameCommand command = GameCommand.from(userCommandInput);
    if (isExitCommand(command)) {
        chessGameService.saveChessGame(ChessGameSaveRequestDto.from(chessGame));
    }
    if (!chessGame.isPlayable()) {
        return;
    }
    if (userCommandInput.equals("status")) {
        Map<Side, Double> scores = chessGame.calculateScores();
        outputView.printGameScores(scores);
        outputView.printWinner(chessGame.calculateWinner());
    }
    //move
    try {
        Position sourcePosition = Position.of(inputs.get(SOURCE_FILE_INDEX), inputs.get(SOURCE_RANK_INDEX));
        Position targetPosition = Position.of(inputs.get(TARGET_FILE_INDEX), inputs.get(TARGET_RANK_INDEX));

        chessGame.move(sourcePosition, targetPosition);
    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }

}
private void play(ChessGame chessGame) {
    List<String> userCommandInput = repeatBySupplier(inputView::requestUserCommandInGame);
    try {
        GameCommand command = GameCommand.from(userCommandInput);
        commands.get(command).accept(chessGame, userCommandInput);
    } catch (IllegalArgumentException e) {
        outputView.printErrorMessage(e.getMessage());
        play(chessGame);
    }
}

패턴 적용 이후 별도의 분기문 없이 명령어 별 로직을 수행할 수 있게 되었다.

이제 새로운 명령어가 필요하면 EnumMap에 추가하고, 해당 명령어가 수행할 메서드만 생성하면 손쉽게 명령어를 추가 할 수 있습니다!

'OOP' 카테고리의 다른 글

DAO(Data Access Object)  (4) 2023.05.01
다형성이란?  (4) 2023.03.11
[OOP] SOLID - DIP(의존성 역전 원칙)  (2) 2022.12.23
[SOLID] ISP(인터페이스 분리 원칙)  (0) 2022.06.07
[OOP] SOLID - LSP(리스코프 치환 원칙)  (0) 2022.06.05
Comments