Skip to content

Conversation

@jongmee
Copy link

@jongmee jongmee commented Mar 28, 2024

안녕하세요 오리! 안내 주신 대로 3단계 먼저 PR 보냅니다 ☺️

3단계에서 추가된 기능

  1. 게임 순서에 따라 사용자가 입력하도록 한다.
  2. 기물마다 점수가 존재해, 사용자가 status를 입력하면 각 진영의 총 점수를 출력한다.
  3. 한 쪽 진영의 King이 잡히면 게임이 종료된다.

변경된 부분

controller 패키지에서 상태 패턴 사용

ChessGame의 역할이 커지는 것 같아 게임의 상태를 클래스로 분리해서 체스게임 어플리케이션을 실행하도록 했습니다. 패키지 이름을 고민했는데, 'game'으로 하기에는 'domain' 패키지와 혼동될 것 같아(ex. game은 도메인 클래스가 아닌가?) 'controller'를 유지했습니다. 게임의 상태들 역시 도메인(Model) 클래스들과 UI(View) 클래스들 사이에서 어플레이케이션을 제어하기 때문에, MVC 패턴에서 Controller의 역할을 수행한다고 생각했기 때문입니다.

PieceValue 인터페이스 생성

Pawn과 다른 기물들 모두 기물 가치(점수)를 가지고 있습니다. 이를 PieceValue로 추상화하고 Pawn과 다른 기물들에 대한 기물 가치를 구현하였습니다.

ChessBoardGenerator 인터페이스 생성

기존에 존재하던 ChessBoardInitializer의 인터페이스입니다. 체스판 생성을 유연하게 변경할 수 있도록 인터페이스를 생성하였습니다. 실제로 ChessBoard를 테스트할 때 TestChessBoardGenerator를 구현하여 쉽게 테스트할 수 있었습니다.

3단계도 잘 부탁드립니다!

jongmee added 27 commits March 26, 2024 18:42
Copy link

@jinyoungchoi95 jinyoungchoi95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 미아 😄

3단계 잘 구현해주셨네요. 몇가지 코멘트 남겨두었으니 확인부탁드려요.

궁금하거나 고민이 되는 부분이 있으시다면 언제든 pr 코멘트 또는 dm으로 요청 부탁드립니다.
감사합니다 🙇‍♂️


import java.util.Map;

public interface ChessBoardGenerator {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 존재하던 ChessBoardInitializer의 인터페이스입니다. 체스판 생성을 유연하게 변경할 수 있도록 인터페이스를 생성하였습니다. 실제로 ChessBoard를 테스트할 때 TestChessBoardGenerator를 구현하여 쉽게 테스트할 수 있었습니다.

ChessBoard의 생성자

    public ChessBoard(Map<Position, Piece> board) {
        this.board = new HashMap<>(board);
    }

에서 테스트에 필요한 원하는 position이 담긴 ChessBoard를 생성해도 될텐데 Generator를 추상화한 목적이 불분명하지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에는 Map을 직접 생성하고 아래와 같이 ChessBoard를 생성하여 테스트하였습니다.

    private ChessBoard createCustomChessBoard(Knight knight, Position knightPosition) {
        Map<Position, Piece> chessBoard = Arrays.stream(File.values())
                .flatMap(this::createPositionStream)
                .collect(toMap(identity(), position -> Blank.INSTANCE));
        chessBoard.put(knightPosition, knight);
        chessBoard.put(knightPosition.calculateNextPosition(0, 1), Pawn.from(Side.WHITE));
        return new ChessBoard(chessBoard);
    }

    private Stream<Position> createPositionStream(File file) {
        return Arrays.stream(Rank.values())
                .map(rank -> Position.of(file, rank));
    }

빈 보드에 기물 2개만 생성하고 있는데, 코드 길이도 길고 가독성도 떨어진다고 생각했습니다. 아래와 같이 특정 체스 보드 상황으로 초기화하고 싶은 경우는 차라리 Fixture를 List로 생성해놓고 사용하는게 이해가 더 쉬울 거 같다고 생각했습니다.

    /*
      ChessBoardFixture.java
      바보 체크메이트(Fool's Mate) 상황
    */
    public static final List<Piece> WHITE_FOOLS_MATE_LOSE = List.of(
            Rook.from(BLACK), Knight.from(BLACK), Bishop.from(BLACK), Blank.INSTANCE, King.from(BLACK), Bishop.from(BLACK), Knight.from(BLACK), Rook.from(BLACK),
            Pawn.from(BLACK), Pawn.from(BLACK), Pawn.from(BLACK), Pawn.from(BLACK), Pawn.from(BLACK), Pawn.from(BLACK), Pawn.from(BLACK), Pawn.from(BLACK),
            Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE,
            Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Pawn.from(BLACK), Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE,
            Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Pawn.from(WHITE), Blank.INSTANCE,
            Pawn.from(WHITE), Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Blank.INSTANCE, Pawn.from(WHITE), Blank.INSTANCE, Blank.INSTANCE,
            Blank.INSTANCE, Pawn.from(WHITE), Pawn.from(WHITE), Pawn.from(WHITE), Pawn.from(WHITE), Blank.INSTANCE, Blank.INSTANCE, Pawn.from(WHITE),
            Rook.from(WHITE), Knight.from(WHITE), Bishop.from(WHITE), Queen.from(WHITE), Queen.from(BLACK), Bishop.from(WHITE), Knight.from(WHITE), Rook.from(WHITE)
    );

따라서 List를 Map<ChessPosition, Piece>로 변환해주는 TestChessBoardGenerator를 테스트에 사용했습니다. 프로덕션 코드에서도 초기 체스판 구성이 바뀔 경우 새로운 ChessBoardGenerator 구현체를 생성하면 될 것입니다.

Generator를 추상화한 저의 의도는 위와 같은데, ChessBoard의 생성자에서 Map<ChessPosition, Piece>을 받을 수 있기 때문에 추상화 목적으로는 충분하지 않다고 보시나요 ?? 오리의 생각이 궁금합니다 !

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChessBoard가 만약 Map<ChessPosition, Piece>를 들고 있지 않고 Generator구현체만을 생성자에서 들고 있을 때 그걸 테스트하기 위해서 추상화한다면 미아가 이야기한 상황이 맞겠죠. 다만 말씀하신대로 ChessBoardMap<ChessPosition, Piece>를 받을 수 있는데 Generator를 추상화하는 건 서로 목적이 다른 것 같아요.

테스트를 위해서 추상화한다는 목적은 이해하였지만 클래스의 단위테스트를 진행할 때는 최대한 다른 클래스와의 의존성은 필요한만큼만 사용할 수 있도록 하는 것이 좋아요. 그래야 변경의 여파가 서로에게 영향이 안가는 테스트를 작성할 수 있습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다음 step에서 더 고민해보고 질문 남기겠습니다!


@Override
public PieceValue value() {
return new CommonValue(9);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

점수 9와 같은 값들은 의미있는 상수로 변경할 수 있겠네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각 기물 클래스 모두 기물 가치 값을 상수화 하였습니다. PieceValue는 값이 다양해서 캐싱하려면 그 값들을 모두 알아야하기 때문에 캐싱하지 않았는데요! 각 Piece에서 상수로 PieceValue 객체 자체를 가지고 있는 것에 대해서 어떻게 생각하시나요??

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PieceValue에서 값이 다양해서 미리 캐싱을 해두지 못한다면 Cache를 두고 getOrPut과 같은 방법으로 이미 생성 요청이 들어왔던 PieceValue에 대해서는 재사용하는 방법도 있을 것 같아요.

각 Piece에서 상수로 PieceValue 객체 자체를 가지고 있는 것에 대해서 어떻게 생각하시나요??

좋은 것 같아요 :)


@Override
public PieceValue value() {
throw new UnsupportedOperationException("빈칸은 기물 가치(Piece Value)가 없습니다.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외의 경우에는 가능하면 테스트가 작성되었으면 합니다 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank에 대해서도 테스트를 추가하였습니다!

public abstract Path findPath(ChessPosition source, ChessPosition target, Piece targetPiece);
public abstract Path findPath(Position source, Position target, Piece targetPiece);

public abstract PieceValue value();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PieceValuevalue보다는 point와 같은 점수임을 나타내는게 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

point나 score도 좋지만 체스에서 '기물 가치(PieceValue)'라고 부르는 것 같아 해당 용어를 사용해보았습니다..!

앞서 언급한 것처럼 체스의 각 기물들은 서로 다른 가치를 가지고 있습니다. 기물 가치는 기물들의 강함의 정도를 나타냅니다. 폰은 1점, 나이트와 비숍은 3점, 룩은 5점, 퀸은 9점입니다.

https://www.chess.com/ko/terms/chess-piece-value-ko

import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;

public class PawnValue implements PieceValue {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 Pawn에 한정되어있는 점수객체라면 pointunfavorablePoint를 받아서 생성하기보다는 상수로 들고 있어도 되지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 PieceValue 의 역할은 각 기물의 가치 계산을 하는 것인데요..! Pawn이 두 점수를 상수로 들고 있다면 Pawn 내부에서 가치 계산을 해주는게 좋을 것 같은데, 1) Piece의 역할이 커지는 것 같고 2) 기물 가치 계산을 evaluation 패키지가 담당하도록 분리하였습니다.

오리는 Piece에서 기물 가치 계산을 하는 게 더 역할이 잘 분배된 구조라고 보시나요 ??

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pawn이 들고있다라기보다는 PawnValue라는 구현체가 이미 pawn의 점수를 가지고 있는 네이밍과 책임을 들고 있기 때문에 PawnValue가 두 값을 상수로 들고 있는걸 말씀드린 것이었어요.

역할 분배자체는 PieceValue로 충분히 잘되어있다고 생각합니다.

return Turn.from(side.getOppositeSide());
}

public boolean isNotCorrect(Piece piece) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"옳지 않다"라는 이름이 이 메소드를 잘 표현해주고 있지 않은 것 같아요. 더 명확한 네이밍은 없을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Piece가 차례에 맞지 않는 것이므로 doesNotMatch로 변경해봤습니다! 저는 괜찮은 것 같은데 명확하다고 생각하시나요 ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 괜찮은 것 같아요 :)

throw new IllegalArgumentException("소스 위치에 기물이 존재하지 않습니다.");
}
if (turn.isNotCorrect(sourcePiece)) {
throw new IllegalArgumentException("올바른 게임 차례가 아닙니다.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무엇이 올바른지 명확하게 명시하는게 어떨까요? 예외 메시지는 명확하면 명확할수록 예외가 발생했을 때 어떤 문제가 발생했는지 알려줄 수 있어요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"소스 위치에 있는 기물이 현재 게임 차례에 맞지 않습니다."라고 변경해보았습니다

import chess.view.input.InputView;
import chess.view.output.OutputView;

public interface GameState {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

ChessBoardGenerator chessBoardInitializer = new ChessBoardInitializer();
ChessBoard chessBoard = new ChessBoard(chessBoardInitializer.create());
outputView.printChessBoard(chessBoard);
return new Run(chessBoard, Turn.from(Side.WHITE));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(반영하지 않으셔도 됩니다.) new Run(chessBoard, Turn.from(Side.WHITE))는 Run의 첫턴이기 때문에 Run에서 첫턴에 해당하는 네이밍을 가진 생성 메소드를 정적팩토리메소드에서 제공할수도 있겠네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래와 같이 정적팩토리메소드를 생성했습니다!

    public static Run initializeWithFirstTurn(ChessBoard chessBoard) {
        return new Run(chessBoard, Turn.from(Side.WHITE));
    }

첫번째 차례를 더 분명하게 보여줄 수 있는 것 같습니다 👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GameState에 대한 정리도 있었으면 좋겠네요 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GameState에 따라 비즈니스 기능이 구분되도록 수정하였습니다!

Copy link

@jinyoungchoi95 jinyoungchoi95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 미아 😄

피드백 잘 반영해주셨네요. 다음단계 진행을 위해 머지하겠습니다. 남겨둔 코멘트에 궁금하신점이 있다면 다음 pr에 함께 올려주세요.

궁금하거나 고민이 되는 부분이 있으시다면 언제든 pr 코멘트 또는 dm으로 요청 부탁드립니다.
감사합니다 🙇‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants