우테코의 미션은 계속 누적된다.
첫주차부터 했던 페어 프로그래밍, 단위 테스트, TDD 모두 누적해서 미션을 구현했다.
💋 상태 패턴 사용
피드백2 강의를 듣기까지 구현을 못했는데, 그래서 이미 상태 패턴을 사용하는 방법을 강의에서 봐 버렸다.
그냥 본 김에 네오를 따라 상태 패턴을 사용해봤다.
장단점을 좀 느꼈는데, 현재까지 내가 느낀 것은 아래 내가 코드리뷰에 남겼던 이야기대로다.
상태 패턴에 대해서 한 번 포스팅 해야겠다!
💋 클래스, 메서드의 abstract, final 제어자를 잘 활용하자
이번 미션에서 상속을 자주 활용하다 보니 제어자 abstract, final에 대해 제대로 이해할 수 있었다.
앞으로 모든 클래스를 abstract나 final 둘 중 하나로 만들기로 다짐했다!
💋 쉽게 계산할 수 있는 내용을 중복해서 필드에 놓지 말자
몇 번의 프로그래밍을 통해서 부수 효과의 위험이라는 것에 대해 좀 더 체감하게 되어서, 이번 1단계에서 페어가 부수 효과를 만들던 것을 곧바로 캐치해서 설득할 수 있었다. 확실히 자동차 경주 때보다 네비게이터로 있을 때에 더 충실하게 잘 하고 있는 것 같다고 생각이 들어서 조금 뿌듯했다! 처음 뼈대는 아래 내용과 같다.
public class Player {
private final Name name;
private final List<Card> cards;
private final int score;
public Player(final Name name, final List<Card> cards) {
this.name = name;
this.cards = cards;
this.score = calculateScore();
}
public void receiveCard(Card card) {
this.cards.add(card);
}
public void updateScore() {
this.score = calculateScore();
}
private int calculateScore() {
...
}
}
페어와 나는 처음에는 Name, List<Card>, Score를 모두 필드에 넣고 싶었다. Player의 중요한 특성이라고 생각했기 때문이다. 겁나 괜찮은 코드 아닌가..? 하지만 위와 같은 코드는 여러 가지 문제가 있다.
카드를 추가로 받고 나서, 반드시 updateScore()를 호출해야 한다. 호출하지 않는 경우에 사용자의 현재 상태가 업데이트되지 않는 문제가 발생할 수 있다. 당연히 호출하는거 아니야? 생각할 수도 있지만, 이 코드를 보는 다른 개발자는 저 메서드의 존재 자체도 모를 수 있고, 심지어는 다음 날의 나 조차도 이 메서드를 호출해야 한다는 사실조차 까먹을 수도 있다.
그러면 그냥 업데이트도 자동으로 하면 되는거 아닌가? 하며 페어가 수정한 코드의 뼈대는 아래와 같다.
public class Player {
private final Name name;
private final List<Card> cards;
private final int score;
public Player(final Name name, final List<Card> cards) {
this.name = name;
this.cards = cards;
this.score = calculateScore();
}
public void receiveCard(Card card) {
this.cards.add(card);
updateScore();
}
private void updateScore() {
this.score = calculateScore();
}
private int calculateScore() {
...
}
}
이 방식은 그럼 완전 편하고 완전 좋은 것 같은데..? 어떤 문제가 있을까?
receiveCard() 메서드에 집중해보자. 메서드의 이름은 카드를 받는다인데, 점수를 업데이트하는 행위를 내맘대로 하고있다. 당장은 문제가 되지 않지만, 외부에서 호출하는 사람은 이런 결과까지 있다는 것을 예상할 수가 없다. 따라서 혼란을 유발할 수 있는 부수효과라고 판단했다.
그러면 메서드 이름을 겁나 예측 가능하게 receiveCardAndUpdateScore()라고 하면 되는거 아닌가??? 생각할 수 있다. 그런데 저 메서드 이름의 문제는 뭘까? 메서드는 최대한 작은 일을 하도록 분리해야 한다. 그래야 여기 저기 재활용도 가능하고, 테스트도 가능하다. 그렇다면 이 클래스를 어떻게 바꿀 수 있을까...?
public class Player {
private final Name name;
private final List<Card> cards;
public Player(final Name name, final List<Card> cards) {
this.name = name;
this.cards = cards;
}
public void receiveCard(Card card) {
this.cards.add(card);
}
public int calculateScore() {
...
}
}
이 코드는 최종적으로 결정된 클래스의 뼈대이다. 변경 사항으로는, 아예 Score라는 필드를 빼 버렸다. List<Card>를 통해 Score는 언제든 계산해낼 수 있기 때문에 이 두 가지를 동시에 갖고 있는 것은 불필요한 필드의 중복이었다고 판단했다. 따라서 과감히 버려버렸다.
결과적으로, Score를 매번 업데이트할 필요가 없어졌다. 대신, 필요한 순간에 Score를 계산해서 호출할 수 있게 되었다.
지금까지의 경험을 바탕으로는 쉽게 계산할 수 있는 내용은 중복 필드로 놓지 않는 것이 더 좋은 방법이라고 생각한다. 하지만 만약, Score를 계산하는 과정이 한 번에 호출해서 띡 받을 수 없이 무거운 작업이라면 어떻게 해야 할까..? 변화가 있을 때만 새롭게 계산하고 변화가 없이 단순 계산결과를 호출하는 것이라면 매번 새로 연산할 필요가 있을까? 난 이런건 나중에 생각하기로 했다.
💋 코드 저장소
https://github.com/woowacourse/java-blackjack/pull/384
https://github.com/woowacourse/java-blackjack/pull/505
이건 크루들끼리 리뷰 받은 PR 링크다!
https://github.com/woowacourse-code-review-study/java-blackjack/pull/7
https://github.com/woowacourse-code-review-study/java-blackjack/pull/16
https://codingnuri.com/seven-virtues-of-good-object/
'우아한테크코스5기' 카테고리의 다른 글
[우테코] 레벨1을 끝내고 답해보자(2): TDD(Test Driven Development), 자바를 잘하는 것이란?, 컴파일 예외 vs 런타임 예외, 예외의 복구/회피/무시/전환 (0) | 2023.03.27 |
---|---|
[우테코] 레벨1을 끝내고 답해보자(1): 단위 테스트, 코드 품질 (0) | 2023.03.27 |
[우테코] 체스 미션 1, 2단계 리팩터링 회고 (feat. 네오의 피드백 강의) (2) | 2023.03.21 |
[우테코] 체스 미션 1, 2단계 회고: 레벨1의 마지막 미션, 3인 페어 후기, 도메인의 예외 처리는 어디까지, TDD의 감잡기, 파라미터로 Optional을 받는 것은 안티패턴 등등... (11) | 2023.03.18 |
[우테코] 데이터베이스와 기초 SQL (0) | 2023.03.08 |