-
Notifications
You must be signed in to change notification settings - Fork 79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[1단계 - 장기] 헤일러(민서현) 미션 제출합니다. #10
Conversation
안녕하세요 로키! 💬 코멘트에 대한 답변
제가 처음 작성한 코드가 조금 난해하기도 하고, Node를 Board 밖으로 꺼내서 여기저기 남용해서 이해하기 어려우셨을 것 같습니다 😭 인접 리스트 방식을 사용한 이유 (노드, 엣지)Point(포인트)public record Point(int row, int column) {
} Point는 단순히 장기판에서의 좌표(row, column)만을 의미합니다. 좌표 그 이상의 역할을 하지 않습니다. Node(노드)public class Node {
private final List<Edge> edges;
}
Edge(엣지)public class Edge {
private final Node nextNode;
private final Direction direction;
public Edge(final Node nextNode, final Direction direction) {
this.nextNode = nextNode;
this.direction = direction;
}
} Edge는 도착 지점의 노드 정보(nextNode)와, 방향 정보(Direction)를 갖고 있습니다. @Test
void 간선이_있는_방향은_true_없는_방향은_false를_반환한다() {
// given
final Node upNode = new Node();
final Node upRightNode = new Node();
final Node centerNode = new Node();
final Node rightNode = new Node();
final Edge rightEdgeForUpNode = new Edge(upRightNode, RIGHT);
final Edge downEdgeForUpNode = new Edge(centerNode, DOWN);
upNode.addAllEdges(List.of(rightEdgeForUpNode, downEdgeForUpNode));
final Edge leftEdgeForUpRightNode = new Edge(upNode, LEFT);
final Edge downEdgeForUpRightNode = new Edge(rightNode, DOWN);
upRightNode.addAllEdges(List.of(leftEdgeForUpRightNode, downEdgeForUpRightNode));
final Edge upEdgeForCenterNode = new Edge(centerNode, UP);
final Edge rightEdgeForCenterNode = new Edge(rightNode, RIGHT);
centerNode.addAllEdges(List.of(upEdgeForCenterNode, rightEdgeForCenterNode));
final Edge upEdgeForRightNode = new Edge(upRightNode, UP);
final Edge leftEdgeForRightNode = new Edge(centerNode, LEFT);
rightNode.addAllEdges(List.of(upEdgeForRightNode, leftEdgeForRightNode));
// when & then
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(upNode.hasNextNode(UP)).isFalse();
softly.assertThat(upNode.hasNextNode(RIGHT)).isTrue();
softly.assertThat(upNode.hasNextNode(DOWN)).isTrue();
softly.assertThat(upNode.hasNextNode(LEFT)).isFalse();
softly.assertThat(upRightNode.hasNextNode(UP)).isFalse();
softly.assertThat(upRightNode.hasNextNode(RIGHT)).isFalse();
softly.assertThat(upRightNode.hasNextNode(DOWN)).isTrue();
softly.assertThat(upRightNode.hasNextNode(LEFT)).isTrue();
softly.assertThat(centerNode.hasNextNode(UP)).isTrue();
softly.assertThat(centerNode.hasNextNode(RIGHT)).isTrue();
softly.assertThat(centerNode.hasNextNode(DOWN)).isFalse();
softly.assertThat(centerNode.hasNextNode(LEFT)).isFalse();
softly.assertThat(rightNode.hasNextNode(UP)).isTrue();
softly.assertThat(rightNode.hasNextNode(RIGHT)).isFalse();
softly.assertThat(rightNode.hasNextNode(DOWN)).isFalse();
softly.assertThat(rightNode.hasNextNode(LEFT)).isTrue();
softly.assertThat(centerNode.hasNextNode(UP_RIGHT)).isFalse(); // 중요
});
} 왕, 사, 병, 차, 포와 같이 방향 기반으로 움직이는 기물들은 노드와 노드 사이가 엣지로 연결된 경로로만 이동 가능합니다.
노드 엣지 기반으로 구현한 경우, 아래와 같은 코드로 "(5,5) 노드"까지만 이동 가능하다는 것을 쉽게 알 수 있습니다. Node currentNode = "(2,2) 노드"; // pseudo code
Direction direction = DOWN_RIGHT;
List<Node> candidates = new ArrayList<>(); // 이동 가능한 노드 후보
while(currentNode.hasNextNode(direction)) {
currentNode = currentNode.getNextNodeByDirection(direction);
candidates.add(currentNode);
} "(5,5) 노드".hasNextNode(DOWN_RIGHT)은 false이기 때문에 이동 가능 후보군(candidates)에 들어갈 수 없습니다. 장기판 격자의 그림이 어떻게 바뀌어도 방향 기반의 기물들은 본인이 움직일 수 있는 방향대로 움직이고 싶은 만큼 움직일 수 있습니다. 즉, 장기판의 대각선 경로가 궁성이 아닌 다른 어디에 생겨도 병, 차, 왕, 사, 포는 본인의 움직임 패턴대로 움직일 수 있다는 장점이 있습니다. 이런 이유로 인접 리스트 표현 방식을 채용해보았습니다. 혹시 설명이 이해되셨을까요! 😄 ❓ 여쭤보고 싶은 점1️⃣ "상"과 "마"의 움직임 관리public record Movement(List<Path> obstaclePaths, Path destinationPath) {
} "도착점과 도착점에 가기 위해 확인해야 하는 장애물들의 위치는 관련이 깊기 때문에 묶어서 관리해야 한다."라는 관점에서 Movement라는 클래스로 2️⃣ 계층에 따라 일관되게 예외 처리하는 것이 괜찮을까요?예외를 처리하는 방법은 복구, 회피, 전환(의도된 회피), 무시 이렇게 4가지 방법이 있다고 배웠습니다.
지금까지 도메인 내의 로직에서 예외가 발생하는 경우 전환 방식을 채용하고, 3️⃣ 전체적으로 방어적 검증(?)이 과하지는 않은지 피드백 받고 싶습니다.class Board {
...
public Point getNextPoint(final Point point, final Direction direction) {
validateExistPoint(point);
Node node = pointNodeMapper.getNodeByPoint(point);
Node nextNode = node.getNextNodeByDirection(direction);
return pointNodeMapper.getPointByNode(nextNode);
}
} 위에서 private List<Point> findMovablePoints(final Point point, final Board board) {
return DIRECTIONS_BY_TEAM.get(this.team).stream()
.filter(direction -> board.existsNextPoint(point, direction)) // 이미 검사함
.map(direction -> board.getNextPoint(point, direction))
.filter(nextPoint -> !(board.existsPiece(nextPoint) && board.matchTeam(nextPoint, this.team)))
.toList();
} 4️⃣ 생성자에 넣기에는 코드의 양이 너무 많을 때 어떻게 하시나요?PointNodeMapper 기본 생성자에서 정석 장기판의 모양에 맞게 Point와 Node의 연관관계를 매핑해주기엔 코드의 양이 너무 많아서 PointNodeMapperFactory 클래스를 따로 분리했습니다... 😭 |
안녕하세요 로키 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헤일러 피드백 반영 잘해주셨습니다. 👍💯
친절하고 꼼꼼하게 구현 의도 설명해주셔서 조금 복잡해보였던 로직이 하나씩 이해되기 시작했어요. 🤩
질문에 답변을 드리다보니 코멘트가 좀 많아졌는데, 1단계 충분히 잘 구현해주신 것 같아서,
2단계 진행하면서 같이 수정해주셔도 좋을 것 같아서 1단계는 이만 머지하도록하겠습니다. 😃
질문에 대한 답변과 추가 코멘트 남겨두었으니 확인 부탁드릴게요. 😉
return !isTwoWangsAlive(); | ||
} | ||
|
||
private boolean isTwoWangsAlive() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
잘 생각해보면 isEnd의 반대니까 isPlaying()
같은 네이밍을 지어도 되지 않을까요?
두개의 왕이 살아있다
는 네이밍 보다는 진행중이다
가 조금 더 의도를 직관적으로 이해할 수 있지 않을까 싶어요. 😃
|
||
private Set<Team> findTeamsOfWang() { | ||
return pieceByPoint.values().stream() | ||
.filter(piece -> piece.type() == PieceType.WANG) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
piece 객체에게 왕인지 여부를 메시지를 보내서 물어보면 어떨까요? 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 piece 객체에게 메시지를 보내서 물어보는 방법은 아래의 3가지가 떠오릅니다.
1번 방법: boolean isWang()
, boolean isPo()
를 추가한다.
piece.isWang();
2번 방법: boolean hasType(PieceType pieceType)
을 추가한다.
piece.hasType(PieceType.WANG);
3번 방법: 현행 유지
step2.1에서 기물의 종류에 따라 점수를 부여해야 하기 때문에, 모든 기물들의 구체 클래스 타입을 확인해야 하므로 enum PieceType을 사용하지 않기는 어려울 것 같습니다. PieceType type()
기능이 있는데 이를 이용하지 않고 인터페이스에 boolean isWang()
을 추가해서 사용하는 것은 인터페이스에 중복된 기능이 추가되는 느낌을 받았습니다. 로키는 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다시 생각해보니 추상 메서드로 int score()
를 둔다면 PieceType을 사용하지 않아도 각 기물의 점수를 다형성을 이용해서 계산할 수 있겠네요..! 좀 더 고민해보겠습니다.
} | ||
|
||
private boolean isTwoWangsAlive() { | ||
return findTeamsOfWang().containsAll(List.of(Team.CHO, Team.HAN)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가적으로 워딩을 생각했을 때, 아래와 같은 로직이 네이밍이랑 조금 더 매칭이 잘되지 않을까요? 🤔
return findTeamsOfWang().containsAll(List.of(Team.CHO, Team.HAN)); | |
findTeamsOfWang().size() == 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장기의 원래 룰대로라면 각 팀의 왕은 1개씩만 존재해야 하지만, 아래와 같은 예외 상황이 발생할 수 있다고 생각해서 팀의 종류까지 확인하게 했어요!
- 한 팀의 왕이 2개 이상이 존재할 수 있는 경우
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Objects; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용하지 않는 import문인 것 같아요. 👀
return piece.canMove(source, destination, this); | ||
} | ||
|
||
public void movePiece(final Point source, final Point destination) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 해보았는데 move와 6,2 사이에 공백이 2개 입력되어서 해당 예외가 발생한 것 같아요!
커맨드와 좌표 사이에 여러 공백이 들어와도 하나로 처리할 수 있는 방법을 생각해보겠습니다. 🤔
return false; | ||
} | ||
Piece piece = getPieceByPoint(point); | ||
return piece.type() == PieceType.PO; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
piece 객체에게 PO와 타입이 같은지 메시지를 보내볼 수 있을 것 같아요.
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
public class PointNodeMapper { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 객체가 필요한 이유도 제가 잘 이해했는지 모르겠는데요. 😭 😭
결국 이 객체는 특정 A 포인트에서 B 포인트로 가는 경로를 찾는데 이용이 되는 걸까요?
(PointNodeMapper 라는 네이밍보다는 조금 더 역할을 드러낼 수 있는 네이밍을 사용해보면 좋을 것 같아요, ex. 경로를 찾는 역할을 한다면.... PathFinder
)
A -> x1 -> x2 -> x3... -> B
의 경로를 찾을 수 있으니 장기말이 이동할 때, 지나가는 경로에 말이 있다면 이동하지 못하는 등의 판단을 하기위함의 용도가 맞는걸까요? 👀
nodeByPoint 와 pointByNode는 단순히 key, value를 뒤바꾼 케이스인 것 같은데요.
pointByNode가 필요한 이유가 있나요?
return findHurdle(nextPoint, direction, board); | ||
} | ||
|
||
private void findCandidates(final Point currentPoint, final Direction direction, final Board board, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 재귀함수를 활용하셨군요? 👍😉
알고리즘 고수의 향기가 물씬나는 헤일러도 이미 잘 아시겠지만 재귀함수는 가독성이 좋다는 장점이 있는 반면에
StackOverFlow 에러가 발생할 위험성이 있어요. 🙄
가능하면 재귀함수 대신에 다른 방법을 고민해보면 어떨까 싶어요.
return new PointNodeMapper(nodeByPoint); | ||
} | ||
|
||
private void createAllNodes(Map<Point, Node> nodeByPoint) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 이해한대로라면 PointNodeMapper
는 몇번을 연산하든 똑같은 결과의 PointNodeMapper가 나올 것 같은데, 차라리 캐싱을 해두는 것도 방법일 것 같아요. 🤔
|
||
public enum JumpingMovements { | ||
|
||
MA( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(질문) 상"과 "마"의 움직임 관리
"도착점과 도착점에 가기 위해 확인해야 하는 장애물들의 위치는 관련이 깊기 때문에 묶어서 관리해야 한다."라는 관점에서 Movement라는 클래스로 List obstaclePaths (장애물들의 위치)와 Path destinationPath (도착점 위치)를 묶어서 관리하고 있습니다. Ma 클래스와 Sang 클래스 맨 위에 static final 상수로 두기에는 가독성을 많이 해치는 것 같아서 JumpingMovements 클래스에서 마상의 가능한 경로 목록을 enum 상수로 관리하도록 분리했습니다. JumpingMovements 클래스의 네이밍은 적절한지, 역할이 너무 없어서 그냥 Ma 클래스나 Sang 클래스 안에 상수로 정의해서 사용하는 것이 더 괜찮을 지 잘 모르겠습니다..!
먼저 JumpingMovements 라는 네이밍의 의도는 무엇인지 궁금합니다. 👀
점핑의 의미가 조금 헷갈리는 것 같아요. 👀 (점핑의 의미가 말을 건너뛰는 의미로 쓰신건가했는데, 상이나 마는 그런 말이 아니었던거 같아서 조금 헷갈리네요. 🙂)
일단 저는 일관성있게 움직임을 정의하면 좋겠다는 생각이 먼저 들기는 합니다. 🤔
저는 현재 마
, 상
만 JumpingMovements를 활용해서 findMovablePoints를 수행하고있는데,
다른 피스들도 일관성있게 JumpingMovements를 활용해서 findMovablePoints를 하도록 해보면 어떨까 싶어요. 🤔
이렇게 일관성있게 코드를 변경한다면 JumpingMovements가 공통적으로 쓰이는 위치를 확인해보면서
적절히 위임할만한 역할이 없는지 살펴보면 좋을 것 같아요. 😃
혹시 제안드린 방식이 너무 어려운 변경이라면 제안드린 방식은 무시해주셔도 될 것 같아요. 😃
안녕하세요! 반가워요 로키
이번에 장기 미션을 진행하면서 몇몇 새로운 시도들을 해보게 되었는데요.
장기라는 게임 자체가 낯설다보니 말의 이동 방식에 따라 말들의 공통된 기능을 묶어내는 것이 굉장히 어려웠던 것 같습니다.
코드가 길어 읽기 힘드시겠지만 리뷰 잘 부탁드립니다 😄
체크 리스트
test
를 실행했을 때, 모든 테스트가 정상적으로 통과했나요?객체지향 생활체조 요구사항을 얼마나 잘 충족했다고 생각하시나요?
1~5점 중에서 선택해주세요.
선택한 점수의 이유를 적어주세요.
어떤 부분에 집중하여 리뷰해야 할까요?
📌 노드와 엣지를 이용한 인접 리스트 구현 방식
장기판을 구현하기에 앞서서, 페어 프로그래밍 첫날은 장기판의 좌표를 어떻게 관리할 지에 대해 고민하며 코드에 거의 손도 대지 않았습니다.
현재 스텝에서는 궁과 궁성에 대한 것을 고려하지 않지만, 각 기물들의 움직임 패턴에 따라 장기판이 어떻게 바뀌어도 동작할 수 있게 만들고 싶었습니다.
그렇게 고민 끝에 노드와 엣지를 이용하여 그래프를 그리게 되었습니다.
제가 생각한 인접 리스트의 장점과 단점은 다음과 같습니다.
BoardGenerator.initializeNodesAndEdges()
)기물들의 움직임이 노드 엣지에 종속적이게 된다는 단점이 있지만, 노드 엣지로 구성된 그래프만 완성되면 보드판의 변경과 확장에 더 유연하다 생각해서 적용해보았습니다. 적절하게 잘 사용했는지 궁금합니다!
📌 기물들의 움직임 패턴과 관련된 상수 관리
각 기물들의 움직임 패턴을
CHO_BYEONG_MOVABLE_DIRECTIONS
,PIECE_PATHS
과 같이 클래스 상단에 static final 상수로 관리했는데, 클래스 윗부분에 하드코딩된 부분이 많아서 가독성을 저하할 수 있겠다고 느껴졌습니다. 이런 부분들을 조금 개선하고 싶은데 어떻게 개선해야 할 지 방법이 잘 떠오르지 않습니다..!