Skip to content
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

[2단계 - 블랙잭 베팅] 구름(김민수) 미션 제출합니다. #675

Merged
merged 50 commits into from
Mar 13, 2024

Conversation

alstn113
Copy link
Member

@alstn113 alstn113 commented Mar 11, 2024

안녕하세요 파랑! 리뷰이 구름 ☁️ 입니다.
우선 1차때 시간에 맞춰 제출하지 못한점 죄송합니다.

제가 궁금한 점은 다음과 같습니다.

⭐️ 딜러와 플레이어가 추상클래스(참여자)를 상속받아서 만든 이유

딜러와 플레이어의 중복 코드를 제거할 수 있는 방법은 총 4가지로 생각했습니다.

  1. 플레이어를 상속 받은 딜러
  2. 행동에 관련된 인터페이스 생성
  3. 행동에 대한 클래스를 만들고 딜러와 플레이어에서 조합해서 사용
  4. 추상클래스를 상속 받은 플레이어와 딜러

1번의 경우 간단한 구조를 가지지만 딜러와 플레이어가 다르게 동작해야하는 경우 적절하지 않다고 생각했습니다.
2번의 경우 관계가 유연해지지만 딜러와 플레이어 모두 구현해야하기에 코드가 복잡해진다고 생각했습니다.

3번과 4번에 대해서 고민을 했습니다.

상속을 하면 부모 클래스가 변할 경우 자식 클래스에도 영향이 간다는 단점이 있었습니다.
하지만 "카드 받기"와 "카드 정보 가져오기"등 공통된 역할을 업캐스팅을 통해 편하게 처리할 수 있어서 4번을 선택했습니다.

파랑은 어떤 것이 더 적합하다고 생각하시나요??

⭐️ CardShape이나 CardRank의 view 정보를 분리해야할까?

도메인에 뷰 정보가 들어 있으면 다음과 같은 단점이 있습니다.

  • 도메인에 뷰 정보가 있으면 뷰가 변경될 경우 도메인이 변경된다.
  • 뷰가 여러 개인 경우 다르게 처리할 수 없다.

이 문제를 해결할 수 있는 방법은 다음과 같습니다.

view에서 CardShape과 CardRank에 대한 enum을 더 만들고 매핑하는 것입니다.

하지만 현재 상태에서 그것을 처리하기에는 복잡도가 너무 많이 올라가서 처리하지 않았습니다.
필요한 상황이 됐을 때 view에 enum으로 처리하는 것이 어떨까요??

⭐️ Controller의 게임 로직들을 분리해야할까?

게임과 관련된 로직들을 BlackjackGame이라는 도메인 혹은 서비스를 만들고 Deck, Bettings를 분리할 수 있다고 생각합니다.
또한 컨트롤러가 아닌 도메인, 서비스 층에서 테스트할 수 있다고 생각합니다.

컨트롤러에서의 테스트는 복잡도가 높아 유지보수의 비용이 크다고 알고 있습니다.
하지만 현재 컨트롤러에서 사용하는 각각의 도메인들이 충분히 테스트되어서 분리할 필요성을 느끼지 못했습니다.

이 부분에 대해서 어떻게 생각하시나요??

⭐️ 승패 판단을 어디서 해야할 지 모르겠다.

  1. 첫번째 승패 판단이 들어 있던 곳은 PlayerGameResult Enum입니다.
    승, 무, 패의 값이 있어서 여기가 처리하기 좋겠다 싶었지만 복잡한 로직이 들어가서 enum의 책임은 아니라고 생각했습니다.

  2. 두번째 승패 판단이 들어 있던 곳은 Dealer 클래스입니다.
    플레이어의 승패를 판단하기 위해서는 딜러의 상태를 알아야하므로 딜러가 판단하는 것이 맞다고 생각했습니다.
    이 부분에서 PlayerGameResult를 Dealer 위치로 그대로 옮겼습니다.
    하지만 승패 판단 로직이 딜러 코드의 절반에 해당해서 굉장히 복잡하다고 생각했습니다.

  3. 현재는 승패 판단 코드를 Judge라는 클래스로 분리해서 사용하고 있습니다.
    Dealer는 Judge라는 클래스를 가지고 플레이어 승패를 판단해주는 메서드가 있습니다.

승패를 판단하기 위해서는 점수가 필요하고, 또 블랙잭 상태가 필요합니다.
또한 플레이어, 딜러에 따라 결과가 다르니 Player, Dealer가 꼭 필요한 것 같습니다.
그래서 자꾸 승패 판단이 다른 객체와 결합도가 높아지네요

이 부분에 대해서 어떻게 생각하시나요??

그 외

마지막으로 캐싱과 상태 패턴을 이용해서 한다는 방법도 생각해봤습니다.
캐싱은 한 번의 게임이고, 덱을 하나만 사용하기 때문에 불필요하다고 생각했습니다.
상태 패턴으로 처음에 만들어 볼려했는데 생각보다 상태를 안가져도 복잡도가 높지 않기에 사용하지 않았습니다.


다시 한 번 1차 제출에 대해서 사과드립니다.

미리 리뷰 감사합니다. 파랑 🔵 ~

Copy link

@summerlunaa summerlunaa left a comment

Choose a reason for hiding this comment

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

구름 안녕하세요~

2단계 빠르게 구현해주셨군요ㅎㅎ 리뷰 남겨드렸으니 확인부탁드립니다 🙇‍♀️

Comment on lines 28 to 29
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {

Choose a reason for hiding this comment

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

equals & hashCode 오버라이딩 👍

@@ -38,17 +41,11 @@ private int calculateHardHandScore() {
}

Choose a reason for hiding this comment

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

.mapToInt(Card::getScore)
.sum();

으로도 수정할 수 있겠네요~!

Copy link
Member Author

Choose a reason for hiding this comment

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

처음에 mapToInt를 사용했다가 reduce가 더 성능이 좋을 것 같았습니다.
확인해보니 reduce가 내부적으로 Integer -> int, int -> Integer를 해서 더 성능이 느리군요!!!
의외네요. 수정하겠습니다.

Choose a reason for hiding this comment

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

네 사실 reduce가 더 빠르다 하더라도 그렇게 가독성이 좋은 것 같지는 않습니다 😅 성능의 차이가 크지 않다면 가독성을 챙기는 것이 더 이득일 수도 있어요.


class Name {
public class Name {

Choose a reason for hiding this comment

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

값객체 👍 동등성이 필요한 객체로 보여서 equals&hashCode를 오버라이딩 해도 좋겠네요


import blackjack.domain.card.Card;
import java.util.List;

public abstract class Participant {

Choose a reason for hiding this comment

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

딜러와 플레이어가 추상클래스(참여자)를 상속받아서 만든 이유
파랑은 어떤 것이 더 적합하다고 생각하시나요??

여러 케이스를 생각해보고 합리적인 판단을 내리신 것 같습니다.
플레이어와 딜러 모두 참가자라는 점(is-a 관계), 공통된 역할이 필요하다는 점 등을 고려해보았을 때 저도 4번을 선택할 것 같네용

@@ -8,6 +8,6 @@ public Player(String name) {

@Override
public boolean isPlayable() {
return !(hand.isBust() || hand.isBlackJack());
return !isBust() && !isBlackJack();

Choose a reason for hiding this comment

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

훨씬 읽기 좋네요 👍

@Nested
@DisplayName("플레이어의 점수가 21점 이하이고, ")
class PlayerScoreLessThanOrEqual21 {
@DisplayName("플레이어의 점수가 딜러의 점수보다 높으면 플레이어가 이긴다.")

Choose a reason for hiding this comment

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

한 줄 띄우면 더 가독성이 좋아질 것 같아요

import org.junit.jupiter.api.Test;

class JudgeTest {
@Nested

Choose a reason for hiding this comment

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

Nested를 이용해서 테스트 코드 읽기가 좋네요 👍

Comment on lines 25 to 39
public void run() {
try {
Participants participants = createParticipants();
Deck deck = Deck.createShuffledDeck();
Bettings bettings = new Bettings();

initialDeal(participants, deck);
placeBetsByPlayers(participants, bettings);
playersTurn(participants.getPlayers(), deck);
dealerTurn(participants.getDealer(), deck);
printResult(participants, bettings);
} catch (IllegalArgumentException e) {
outputView.printErrorMessage(e.getMessage());
}
}

Choose a reason for hiding this comment

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

  • 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.

가능하면 지켜볼까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

수정했습니다.

Comment on lines 48 to 51
for (Participant participant : participants.getParticipants()) {
participant.hit(deck.draw());
participant.hit(deck.draw());
}

Choose a reason for hiding this comment

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

Participant 내부로 넣을 수 있는 로직으로 보여요. participant.hit(deck.draw());의 중복도 줄여볼까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

혹시 Participant에서 public void hit(Deck deck)이 되게 하라는 말씀이실까요?
이 코드의 문제점은 테스트할 때 Card가 아닌 Deck을 만들어야해서 복잡도가 올라가는 것 같습니다.

아니면 Participants에서 2장씩 뽑는 것을 의미하신걸까요?
제 생각에는 Participants의 책임과 거리가 먼 것 같아서 하지 않았습니다...

hit의 파라미터로 count를 넣을까 했는데 뭔가 메서드도 늘어나고 count가 1인 경우 오버로드도 해야해서 뭔가 이상한 느낌이더라구요;;

Choose a reason for hiding this comment

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

participant.hit(deck.draw());을 뽑아야 하는 횟수만큼 코드로 반복하는 것이 좋은 방향은 아니라고 봤어요~
각각의 방법들에 장단점이 있으니 고민해보시면 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

Particiapnts에 위치하게 수정했습니다.

import java.util.List;
import java.util.Map;

public class BlackjackController {

Choose a reason for hiding this comment

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

Controller의 게임 로직들을 분리해야할까?

view까지 domain 로직으로 넘어가지 않는 선에서 controller를 줄일 수 있다면 줄이는 게 좋겠죠~ controller 보다는 domain이 풍부한 게 좋으니까요. 현재 로직에서는 view와 섞인 코드가 많아 분리가 쉽진 않겠네요ㅎㅎ.

Comment on lines 17 to 30
public Hand() {
cards = new ArrayList<>();
}

private Hand(List<Card> cards) {
this.cards = new ArrayList<>(cards);
}

public Hand add(Card card) {
List<Card> newCards = new ArrayList<>(cards);
newCards.add(card);

return new Hand(newCards);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Hand의 add 메서드가 Hand를 리턴하게 해서 Hand를 불변 객체로 수정했습니다.

Comment on lines 7 to 28
public class Card {
private static final Map<String, Card> CARD_POOL = new HashMap<>();

private final CardRank cardRank;
private final CardShape cardShape;

static {
for (CardRank cardRank : CardRank.values()) {
for (CardShape cardShape : CardShape.values()) {
CARD_POOL.put(toKey(cardRank, cardShape), new Card(cardRank, cardShape));
}
}
}

private Card(CardRank cardRank, CardShape cardShape) {
this.cardRank = cardRank;
this.cardShape = cardShape;
}

public static Card of(CardRank cardRank, CardShape cardShape) {
return CARD_POOL.get(toKey(cardRank, cardShape));
}
Copy link
Member Author

Choose a reason for hiding this comment

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

카드 캐싱 기능 추가해봤습니다~

Copy link

@summerlunaa summerlunaa left a comment

Choose a reason for hiding this comment

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

구름 안녕하세요~

제 리뷰 속도가 구름의 반영 속도를 따라가지 못하네요 ...ㅎㅎ ㅠㅠ 빠른 반영과 추가 리팩토링까지~ 수고하셨습니다.

지금도 요구사항을 충분히 만족했지만 아쉬우실까봐 한번 더 RC 드려봅니다~ 파이팅 💪


private static final int MIN_MONEY = 1;

private final int value;

Choose a reason for hiding this comment

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

클래스명과 겹치는 네이밍보다는 value가 좋네요 👍

List<Participant> participants = new ArrayList<>();

participants.add(dealer);
participants.addAll(players);

return participants;
return Collections.unmodifiableList(participants);

Choose a reason for hiding this comment

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

방어적 복사 👍

String message = String.format("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)", name);
String input = readLine(message);

if ("y".equals(input)) {
if (YES.equalsIgnoreCase(input)) {

Choose a reason for hiding this comment

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

YES.equalsIgnoreCase(input)input.equalsIgnoreCase(YES)의 차이를 아시나요 👀

Copy link
Member Author

Choose a reason for hiding this comment

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

input. 으로 하면 NPE가 발생할 수 있습니다!


public class Dealer extends Participant {

private static final String DEALER_NAME = "딜러";

Choose a reason for hiding this comment

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

다시 보니 반드시 딜러의 이름이 "딜러"여야 하는지 의문이 드네요. 콘솔뷰 한정으로 "딜러"라는 이름을 사용한다면 view의 영역으로도 볼 수 있겠네요~!

Copy link
Member Author

Choose a reason for hiding this comment

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

Dealer에서 Name은 기본 이름으로 쓸 수 있다고 생각해서 남겨놨습니다.
뷰에서는 딜러 이름을 처리했습니다.

Comment on lines 14 to 16
for (CardRank cardRank : CardRank.values()) {
for (CardShape cardShape : CardShape.values()) {
CARD_POOL.put(toKey(cardRank, cardShape), new Card(cardRank, cardShape));

Choose a reason for hiding this comment

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

  • indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.

Copy link
Member Author

Choose a reason for hiding this comment

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

솔직히 이중 for문이 더 이해가 쉬운 것 같지만 수정했습니다!

static {
    for (CardRank cardRank : CardRank.values()) {
        generateRankCards(cardRank);
    }
}

private static void generateRankCards(CardRank cardRank) {
    for (CardShape cardShape : CardShape.values()) {
        CARD_POOL.put(toKey(cardRank, cardShape), new Card(cardRank, cardShape));
    }
}

Choose a reason for hiding this comment

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

(공감합니다 ㅋㅎㅎ)

Comment on lines 21 to 24
List<Card> cards = Arrays.stream(CardRank.values())
.map(Deck::createRankCards)
.flatMap(List::stream)
.collect(Collectors.toList());

Choose a reason for hiding this comment

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

리뷰 드리지도 않았는데 캐싱까지 👏 넘 좋네요. 캐싱한 카드들을 여기서도 사용할 수 있을 것 같은데 어떠신가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

Card 클래스에서 캐싱된 전체 카드를 가져오게 한 후 Deck에서 사용하게 했습니다.

// Card.java
public static List<Card> getCards() {
    return new ArrayList<>(CARD_POOL.values());
}
// Deck.java
public static Deck createShuffledDeck() {
    List<Card> cards = Card.getCards();
    Collections.shuffle(cards);

    return new Deck(cards);
}

Copy link

@summerlunaa summerlunaa left a comment

Choose a reason for hiding this comment

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

구름 안녕하세요~

요구사항도 모두 충족하셨고 깔끔하게 잘 완성해주셨습니다 👏 이번 미션은 여기서 머지하겠습니다.

블랙잭 미션도 고생하셨습니다~ 👍 👍

@summerlunaa summerlunaa merged commit 14cb67f into woowacourse:alstn113 Mar 13, 2024
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.

None yet

2 participants