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단계 - 블랙잭 베팅] 로빈(임수빈) 미션 제출합니다! #9

Open
wants to merge 38 commits into
base: main
Choose a base branch
from

Conversation

robinjoon
Copy link
Member

@robinjoon robinjoon commented Mar 14, 2024

기능 변경 사항

승부 판단 규칙 변경

  • 플레이어와 딜러가 모두 bust(21점이 넘는 경우) 플레이어의 승리

베팅 기능

  1. 플레이어는 양의 정수 만큼의 배팅을 할 수 있다.
  2. 게임의 결과에 따라 상금이 달라진다.
    • 블랙잭(처음 뽑은 두개의 카드로 21점)인 경우 : 베팅 금액의 1.5배
    • 블랙잭이 아닌 상태로 승리한 경우 : 베팅 금액의 1배
    • 패배한 경우 : 베팅 금액을 잃는다.
    • 비긴 경우 : 베팅 금액을 그대로 돌려 받는다

고민과 선택

정말 컨트롤러에는 도메인 로직 vs 도메인에 간접적인 뷰의 로직 주입

1단계 머지된 시점에서의 BlackJackController의 일부 코드 입니다.

private boolean playerTryDrawOnce(Deck deck, Player player) {
      boolean needToDraw = YesOrNoInputView.getYNAsBoolean(player.getRawName());
      DrawResult drawResult = null;
      if (needToDraw) {
          drawResult = playerDraw(deck, player);
      }
      printPlayer(player);
      if (drawResult == null) {
          return false;
      }
      return drawResult.hasNextChance();
}

컨트롤러에 플레이어가 카드를 뽑기 원하는지 입력을 받고, 이를 이용해 카드를 뽑는 비즈니스 로직이 포함되어 있습니다.

저는 컨트롤러의 역할이 단순히 모델과 뷰 사이의 통신을 대리하는 것이라 생각합니다. 따라서, 컨트롤러에는 비즈니스 로직이 포함되면 안된다(도메인 로직은 모델에서 수행하므로)고 결론 내렸습니다. 그 결과 다음과 같은 코드가 작성되었습니다.

public class BlackJackGame {
// 생략
    public void playersDraw(Deck deck, PlayerDrawAfterCallBack playerDrawAfterCallBack,
                            DrawConfirmation drawConfirmation) {
        players.draw(deck, playerDrawAfterCallBack, drawConfirmation);
    }
}
public class BlackJackController {
// 생략
    private void playersTryDraw(Deck deck, BlackJackGame blackJackGame) {
        blackJackGame.playersDraw(deck, this::printPlayer, YesOrNoInputView::getYNAsBoolean); 
    }
}

위와 같이 도메인에 함수형 인터페이스를 통해 뷰의 로직이 전달되는 구조가 되었습니다. 코드상에서 도메인이 뷰를 의존하지 않지만 결국 런타임에 도메인이 뷰의 로직을 주입받아 수행한다는 점에서 여전히 의존성이 남아있다고 생각됩니다. 덤으로 매개변수의 개수가 늘어나는 단점도 함께 가지게 되었습니다.

지금 시점에는 도메인 객체의 매개변수 개수가 아직은 감당 가능하고, 잘 동작하며, 코드상에 명시적인 의존성은 사라지기 때문에 도메인에 함수형 인터페이스를 통해 뷰의 로직을 간접적으로 주입받는 것을 선택했습니다.

질문

클래스 설계를 하지 않고 TDD를 하는게 맞나??

이번 미션에서도 네오 강의에서 배운대로 TDD로 미션을 수행했습니다. 네오 강의에서는 별도의 클래스 설계(어떤 클래스가 있고, 어떤 클래스와 어떤 클래스가 어떤 메세지를 주고 받는지) 없이 생각의 흐름대로 진행했습니다.

저번 사다리 타기 미션에서는 이 방식으로 잘 했는데, 이번 블랙잭 미션에서는 이 방식대로 했더니 코드를 리팩토링하는 것이 힘들 정도로 이상한 코드가 작성되었습니다.

코드를 리팩토링하는 것이 힘들 정도로 이상한 코드가 작성된 것이 제 리팩토링 능력 부족이라 그런건지, 애초에 리팩토링이 어렵게 될 수 밖에 없는 방식인건지 궁금합니다.

robinjoon and others added 30 commits March 12, 2024 20:09
* docs: 요구사항 분석 및 기능 목록 작성

* feat: 덱 관리 기능 구현

* feat: 카드 점수 계산 기능 구현

* refactor: CardValue 를 CardName 으로 변경

* feat: 보유한 카드 점수 합계 계산 기능 구현

* feat: 플레이어 및 딜러 점수 계산 기능 구현

* fix: 플레이어가 카드를 뽑을 수 없는 경우에도 카드를 뽑을 수 있던 오류 수정

* fix: 딜러가 카드를 뽑을 수 없는 경우에도 카드를 뽑을 수 있던 오류 수정

* refactor: Player 와 Dealer 통합

* feat: 플레이어 이름 입력 기능 구현

* feat: 카드 받을지 여부 입력 기능 구현

* feat: 플레이어 및 딜러간 승부 계산 기능 구현

* refactor: 플레이어 및 딜러간 승부 계산 기능 Gamer 에서 분리

* feat: 게임 결과 출력 기능 구현

* docs: 기능 목록 정리

* feat: 플레이어 및 딜러 손패 출력 기능 구현

* feat: 게임 승부 결과 출력 기능 구현

* feat: 전체 게임 진행 기능 구현

* fix: 게이머가 죽었을 경우 승부가 잘못 판단되는 오류 수정

* feat: Ace 카드 점수 보정 기능 구현

* docs: TODO 목록 정리

* fix: Main 메서드에서 플레이어 이름을 입력받지 않는 오류 수정

* docs: 딜러 Ace 카드 점수 보정 기능 추가

* feat: 딜러 Ace 카드 점수 보정 기능 구현

* fix: 게임 도중 플레이어의 손패를 출력하지 않는 오류 수정

* fix: 딜러의 카드 한장 숨기도록 수정

* refactor: TODO 처리

* refactor: domain 하위 패키지 추가

* refactor: 클래스 및 패키지 접근지정자 조이기

* refactor: 메서드 분리

* style: 클래스와 제일 위에 위치한 필드(메서드) 사이에 공백 라인 제거

* refactor: 일부 매직 값을 상수 혹은 의미있는 변수로 추출

* fix: 플레이어 이름을 잘못 출력하는 버그 수정

* chore: 람다표현식으로 변경

* docs: 1단계 피드백 반영 예정 목록 추가

* refactor: 드로우가 가능한지 확인하는 책임 컨트롤러에서 Gamer로 이동

* refactor: 드로우 여부 결정하는 정책과 드로우 방식을 결정하는 정책 분리

* refactor: Card Enum화

* refactor: 테스트 코드에서 사용하는 테스트 객체 생성 역할을 분리

* test: 게이머의 점수가 잘 계산되는지 검증하는 테스트 추가

* refactor: view만을 위한 enum 추가

* chore: 필요 없는 TODO 주석 제거

* refactor: Controller 내부 메서드 분리

* fix: 플레이어가 딜러의 전략을 사용하는 오류 수정

* refactor: 딜러와 플레이어 분리

상속이 아니라 조합을 이용함

* refactor: 딜러의 카드 하나 숨기는 기능 Dealer로 이관

* refactor: Main, Controller, View 역할 명확하게 분리

* refactor: Dealer 에서 이름 제거, View로 이관

* refactor: GamerOutputView를 DealerOutputView 와 PlayerOutputView로 분리

* refactor: BlackjackController 내부 메서드 정리

* refactor: Players 일급 컬렉션 추가

* refactor: Players 생성자 변경

* chore: 사용하지 않는 메서드 제거

* refactor: RandomCardSelectStrategy 싱글톤으로 변경

* fix: 출력문구 누락 수정

* refactor: BlackJackGameMachine 내부 메서드 이름 및 접근 지정자 변경

---------

Co-authored-by: BurningFalls <[email protected]>
미션 페이지의 요구사항 문장을 잘못 해석했었다.
@robinjoon
Copy link
Member Author

리뷰어 피드백 반영해서 코드 수정했습니다.

Copy link

@3Juhwan 3Juhwan left a comment

Choose a reason for hiding this comment

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

로빈~~~ 리뷰가 늦었지? 😢
코드 잘 읽었어! 특히 BlackJackGame이 인상적이었어. 나도 시도해 보고 싶었던 설계였거든.
다만 이번에 리뷰를 꼼꼼하게 모든 클래스를 보진 못했는데, 코드가 내가 읽기에 너무 수준이 높달까..

코드를 읽으면서 느낀 점은 다음과 같아. 이 점을 개선해 주면 좋을 것 같아! 로빈 코드 꼭 완벽히 이해하고 싶어 😀

  • 패키지 분리의 이점을 못 느꼈어. blackjack 패키지가 사실상 모든 로직을 담고 있다고 생각해. 패키지 내부에 존재하는 클래스에 package-default 등의 적절한 접근 제어자를 썼더라고. 하지만 패키지가 사실상 전부이고 이런 접근 제어자가 의미가 있을까? 라는 생각이 들더라고
  • 코드를 읽는 진입점을 찾기 어려웠어. 위와 비슷한 맥락이야. 로빈이 세세하게 추상화를 해뒀는데 구현에 의존하는 코드가 많아서 결국 모든 코드를 읽어야 되더라고 😢 적절하게 패키지를 분리하고 인터페이스에만 의존하게 코드를 개선한다면 더 읽기 편할 것 같아!

블랙잭 2단계 고생 많았어!! 머지까지 힘내자구 💪💪

Comment on lines +23 to +27

public final List<Card> getRawHoldingCards() {
return getRawHoldingCards(AllCardShowStrategy.INSTANCE);
}

Copy link

@3Juhwan 3Juhwan Mar 17, 2024

Choose a reason for hiding this comment

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

AllCardShowStrategy 구현에 의존하고 있는데, 이렇게 작성한 로빈의 의도가 궁금해요 💭
비슷하게 WithOutFirstCardShowStrategy도 컨트롤러에서 의존하고 있네요!
CardShowStrategy 인터페이스로 분리한 이점이 무엇이 있나요?

Comment on lines +4 to +8

public final class DealerCardDrawCondition implements CardDrawCondition {
public static final int RAW_DEALER_DRAW_THRESHOLD_POINT = 16;
private final BlackJackGameMachine blackJackGameMachine;

Copy link

@3Juhwan 3Juhwan Mar 17, 2024

Choose a reason for hiding this comment

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

이 부분도 상수를 외부에서 사용하고 있네요!
로빈의 의도를 조금은 이해가 되는데요~ 비즈니스 정책을 관리하기 위함이 맞나요?
맞다면, 이를 위해 인터페이스를 분리하고 여러 구현체를 만든 것으로 파악되는데요.
과한/잘못된 추상화가 아닌가? 라는 생각이 드네요! 외부에서 구현에 의존하기도 하고요.
클래스, enum 등으로 비즈니스 정책을 관리하는 것은 어떨까요? 로빈의 의견이 굉장히 궁금해요 💭

Comment on lines +7 to +9
public class CardPool {
static final Map<CardType, Map<CardName, Card>> pool = new ConcurrentHashMap<>();

Copy link

Choose a reason for hiding this comment

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

동시성 👍

Comment on lines +24 to +28

public static Card get(CardType cardType, CardName cardName) {
return pool.get(cardType).get(cardName);
}
}
Copy link

Choose a reason for hiding this comment

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

테스트에서만 사용하는 것 같은데, 접근 제어자를 수정하면 좋을 것 같아요 💭

Copy link
Member

@reddevilmidzy reddevilmidzy left a comment

Choose a reason for hiding this comment

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

마지막까지 화이팅해봅시당!!

Comment on lines +5 to +8
if (rawBattingMoney < 0) {
throw new IllegalArgumentException("배팅 금액은 음수일 수 없습니다.");
}
}
Copy link
Member

Choose a reason for hiding this comment

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

0원도 배팅가능하게 하신건가요?

Comment on lines +23 to +24
if (rawNumber <= 0) {
throw new IllegalArgumentException("입력 형식이 올바르지 않습니다.");
Copy link
Member

Choose a reason for hiding this comment

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

0이 입력되면 여기서 걸리겠군요

요 예외메시지는 그럼 형식이 올바르지 않다기보다 범위가 올바르지 않은 느낌 아닌가요?!

}

public static void printDealerDrawDone() {
print("딜러는 %d이하라 한장의 카드를 더 받았습니다.\n".formatted(RAW_DEALER_DRAW_THRESHOLD_POINT));
Copy link
Member

Choose a reason for hiding this comment

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

여기도 /n 대신 %n로 변경해주면 좋을 거 같아요

Comment on lines +13 to +17
private static void print(String output) {
System.out.println(output);
}

public static void printDealerDrawDone() {
Copy link
Member

Choose a reason for hiding this comment

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

요 둘 메서드 순서도 public 이 위에 있는게 좋아보여요..!

Copy link
Member Author

Choose a reason for hiding this comment

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

요 둘 메서드 순서도 public 이 위에 있는게 좋아보여요..!

메서드 선언 순서는 종속된 메서드가 바로 아래에 오도록 설정했어요.

Comment on lines +12 to +30
static DrawResult success(boolean hasNextChance) {
return new DrawResult(null, hasNextChance);
}

static DrawResult fail(Exception drawFailCause, boolean hasNextChance) {
return new DrawResult(drawFailCause.getMessage(), hasNextChance);
}

static DrawResult fail() {
return new DrawResult("카드를 더이상 뽑을 수 없습니다.", false);
}

boolean hasNextChance() {
return hasNextChance;
}

boolean isSuccess() {
return failCause == null;
}
Copy link
Member

Choose a reason for hiding this comment

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

success 한다면 failCause에 null 을 넣고 확인을 failCause == null 로 확인하는 것 말고, null 을 사용하지 않는 방법은 없을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

success 한다면 failCause에 null 을 넣고 확인을 failCause == null 로 확인하는 것 말고, null 을 사용하지 않는 방법은 없을까요?

이것 저것 시도해봤는데 null 을 사용하는 것이 가장 명확하고 간단했어요.

import domain.card.RandomCardSelectStrategy;

public class Player extends Gamer {
private final String name;
Copy link
Member

Choose a reason for hiding this comment

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

요 이름도 객체 만들어서 포장해주는건 어떨까요?>

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.

3 participants