[Lv1] 블랙잭 미션 회고
블랙잭 미션 회고
상속 vs 합성
상속
- 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받는다.
- 단순히 메서드나 인스턴스 변수를 재사용하는 것이 아니다.
- 외부 객체는 자식 클래스를 부모 클래스와 같은 타입으로 간주할 수 있다.
- 상속의 단점
-
- 하위 클래스가 부모 클래스의 내부 구조를 잘 알고 있어야 한다. (캡슐화 위반)
- 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정하기 때문에 실행 시점에 객체의 종류를 변경하기 어렵다. (설계가 유연하지 않다.)
-
합성
- 클래스가 다른 클래스의 인스턴스를 멤버 변수로 포함하여 그 기능을 활용하는 방식
- 위임 : 포함된 객체의 메서드를 호출하여 기능을 수행한다.
합성과 상속
- 공통점 : 모두 코드를 재사용할 수 있다.
- 상속은 부모 클래스와 자식 클래스의 코드를 컴파일 시점에 강하게 결합한다.
- 합성은 인터페이스를 통해 약하게 결합되어, 인터페이스에 정의된 메서드로만 코드를 재사용한다.
- 관계
- 상속 : is-a
- 부모 클래스의 코드를 재사용한다.
- 합성 : has-a
- 전체를 포함하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.
- 상속 : is-a
- 차이점 : 합성은 실행시점에 동적으로 관계를 수정할 수 있다. (유연한 설계에 가능하다.)
합성을 무조건 사용하는 것이 좋을까?
전부터 설계 공부를 하면서 "상속보다는 합성을 사용하라" 라는 말을 많이 들었다. 합성을 사용하면 상속보다 유연하기 때문이다. 따라서 이번 블랙잭 미션을 수행하면서 Player
와 Dealer
의 공통 코드를 상위 클래스에서 상속받기 보다, Gamer
라는 객체로 두어 합성하는 방법을 고민했다.
그런데 Player
와 Dealer
는 게임을 수행하는 객체인데, 매번 Gamer
를 의존하는 것이 이상하다는 생각이 들었다. Player
와 Dealer
가 게임을 수행하는 것이 핵심 역할이라면, Gamer
라는 다른 객체가 아닌 Player
와 Dealer
가 가질 책임이지 않을까? 즉, Dealer
와 Player
가 게임을 수행하는 책임을 가져야하는데, Gamer
라는 객체로 책임이 분리되어 비즈니스 로직이 파편화되는 것은 아닐까?
게임 외에 다른 여러 행위를 하였다면 객체들이 Gamer를 의존하는 것이 좋다고 생각한다.
후에 Dealer
와 Player
가 게임 말고 다른 행위를 할 수 있으므로 확장성을 고려하여 합성을 이용하는 것이 좋을지 고민이 되었다.
Player
와 Dealer
는 Gamer
이므로, Gamer
의 하위 타입으로 충분히 판단될 수 있다.Player
와 Dealer
가Gamer
를 상태로 가지는 것보다 같은 타입으로 간주하는 것이 더 자연스러우므로, 합성보다 상속이 더 적합하다는 답변을 해주셨다.
상속 vs 합성 사용기준
상속은 같은 타입으로 간주할 수 있을 때 (is A
), 합성은 행위를 재사용하되 그 클래스의 종류가 되고 싶지 않을 때 (has a
) 사용한다.
합성 사용시 메서드 깊이 (추상화 수준)가 깊어지는 문제
메서드 깊이
public class Participants {
private final Dealer dealer;
private final Players players;
public Participants(final Dealer dealer, final Players players) {
this.dealer = dealer;
this.players = players;
}
public void spreadAllTwoCards(final Cards cards) {
final int size = 2;
dealer.receiveCards(cards.getPartialCards(0, size));
players.receiveCards(cards.getPartialCards(size, cards.getSize()), size);
}
public void spreadOneCardToPlayer(final int index, final Card card) {
final Player player = players.getPlayer(index);
player.receiveCards(new Hand(List.of(card)));.
}
public void spreadOneCardToDealer(final Card card) {
dealer.receiveCards(new Hand(List.of(card)));.
}
Participants
는 Dealer
와 Players
를 가지고 있다.Dealer
와 Players
는 Gamer
의 공통 메서드(spreadOneCard
)를 호출한다.
합성으로 가지고 있는 필드인 Dealer
와 Players
에 대해 외부에서 메서드를 호출해야하기 때문에 Dealer
와 Player
마다 메서드를 생성해야하는 번거로움이 발생했다. 참가자들끼리 공통 로직이 존재하므로 묶었지만, depth
가 깊어지는 문제가 발생해서 리뷰어에게 물어보았다.
합성은 다른 클래스의 인스턴스를 멤버 변수로 포함하여 그 기능을 활용하는 방식이다. 따라서 depth
가 깊어지는 문제는 어쩔 수 없다.
만약 상속을 사용하더라도 부모 클래스의 코드를 알아야하므로 depth가 깊어지는 문제가 발생한다.
집중할 부분은, 만약 상속이나 합성을 사용하지 않고 한 객체가 모든 책임을 가지게 된다면 유지보수 관점에서 좋지 않다는 것이다. 즉, 유지보수성 vs depth
깊이가 깊어지는 문제가 충돌될 때, 유지보수성을 우선시해야한다.
추상화 계층 분리
단순히 합성으로 가진 필드를 호출하는 "위임" 행위이더라도, 객체의 행위적 관점에서는 다른 동작일 수 있다고 하셨다.
Participants.java
public void spreadAllTwoCards(final Cards cards) {
final int size = 2;
dealer.receiveCards(cards.getPartialCards(0, size));
players.receiveCards(cards.getPartialCards(size, cards.getSize()), size);
}
Participants
입장에서는 모든 참가자들에게 카드를 분배하는 행위이지만, dealer
와 players
는 단순히 카드를 받는 행위를 수행한다.
즉, 합성을 통해 고수준, 저수준 추상화 계층을 나누고, 각 객체는 자신의 역할과 책임에 맞게 메서드를 명명할 수 있다.
정적 팩토리 메서드
생성자 vs 정적 팩토리 메서드
- 생성자에 비해 정적 팩토리 메서드는 이름을 지정할 수 있어 의미가 풍부하다.
- 또한 정적 팩토리 메서드는 매번 메서드를 호출할 때마다 객체를 생성하지 않아도 되어 캐싱을 사용하거나, 하위 타입 객체를 반환할 수 있다.
private Participants makeParticipants() {
final Dealer dealer = new Dealer(new Cards(new ArrayList<>())); // 여기 부분
final Players players = makePlayers();
return new Participants(dealer, players);
}
딜러는 초기에 생성될 때 매번 빈 상태의 카드를 가지고 생성된다. 따라서 빈 카드를 생성하는 정적 팩토리 메서드를 만들면 의미가 더 명확하게 전달된다.
public static Player from(final String nickname) {
return new Player(nickname, new Cards(new ArrayList<>()));
}
생성자를 private으로 막아두는 것이 좋을까? (테스트 DI)
현재 초기 객체가 다 빈 카드를 가지고 생성되어, 정적 팩토리 메서드만 사용된다. 따라서 생성자를 private
으로 만들 수 있는데, 테스트 측면에서 어려움이 있다.
생성자를 사용하지 못하면 테스트시 객체의 내부 상태를 지정할 수 없고, 내부 카드를 추가하는 메서드도 존재하지 않아 테스트하기 어렵다.
public static List<Player> provideTwoPlayersWithCards(final Cards cards1, final Cards cards2) {
return List.of(new Player("엠제이", cards1), new Player("밍트", cards2));
}
우선 생성자를 public
으로 열어두었는데, 어떻게 이 문제를 해결할 수 있을지 리뷰어에게 물어보았다.
리뷰어는 보통 주 생성자는 굳이 private
으로 막지 않는 편이라고 하셨다. 따라서 fixture
함수가 주생성자를 대체하는 것은 과도하게 복잡해지는 감이 있어서, 주 생성자는 public
으로 열어두기로 결정했다.
결론
주 생성자는 public
으로 열어두어 테스트 유연성을 제공하자!
하지만 util과 같은 싱글톤 객체의 경우는 private으로 막아두는 것이 좋다.
메서드 순서
리뷰어가 가급적 호출되는 순서대로 메서드를 위치시키면 코드를 파악하는 데에도 도움이 많이 될 것 같다고 하셨다.
클린 코드 - 수직 거리
호출하는 메서드와 호출되는 메서드를 가까이 두는 것이 좋다. 이를 통해 위->아래 순으로 코드를 읽고 파악하는데 편하다.
콘솔 애플리케이션이 MVC 패턴에 적합한가?
- 내용이 길어서 하나의 글로 작성했다.
[블랙잭 미션] MVC 패턴, 서비스가 콘솔 애플리케이션에서 필요한가?
MVC 패턴, 서비스가 콘솔 애플리케이션에서 필요한가?MVC 패턴Model : 애플리케이션의 데이터와 비즈니스 로직 담당View : 사용자에게 보여지는 부분 (데이터의 시각화)Controller : Model로부터 데이터를
shout-to-my-mae.tistory.com
우승 결과 판단 객체
AS IS
BlackjackGame.java
public Map<String, ResultStatus> calculateWinningResult() {
final int dealerScore = participants.calculateDealerMaxScore();
final Map<String, ResultStatus> result = new HashMap<>();
for (Gamer player : participants.getPlayers().getPlayers()) { // getter 사용
final int playerScore = player.calculateMaxScore();
result.put(player.getNickname(), ResultStatus.calculateResultStatus(playerScore, dealerScore));
}
return Collections.unmodifiableMap(result);
}
- 기존에는
Participants
(Dealer, Players) 에서 우승 결과(WIN
/LOSE
) 를 판단하였다. - 그런데 리뷰어가 우승 결과 판단 로직을
Participants
가 아닌Dealer
로 옮기는 것을 제안하였다. 우승 결과를Participants
에서 판단하면 딜러와 플레이어들 중 결과가 누구 기준으로 나오는지 불명확하기 때문이다.
TO BE
Dealer.java
public Map<String, ResultStatus> calculateMaxScore(final Map<String, Integer> playerScores) {
int dealerScore = calculateMaxScore();
return Collections.unmodifiableMap(playerScores.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey,
entry -> ResultStatus.calculateResultStatus(entry.getValue(), dealerScore))));
}
- 블랙잭 게임에서도 결과 판단은
Dealer
가 수행하므로, 결과 판단 책임을Dealer
로 이동하였다.
결론
- 결과를 판단하는 로직은 공통 클래스에 있기 보다 기준을 명확하게 판단할 수 있는 객체에 두자.
도메인 용어 사용하기
- 객체 이름을 지을 때 도메인 지식을 활용하여 이름 지으면 좀 더 명확한 개념을 나타낼 수 있다.
블랙잭 도메인 용어
딜러(Dealer)
: 카드를 나눠주고 게임을 진행하는 역할핸드(Hand)
: 플레이어나 딜러가 받은 카드 세트히트(Hit)
: 추가 카드를 요청하는 행동버스트(Bust)
: 카드 합계가 21을 초과하여 패배하는 상황푸시(Push)
: 플레이어와 딜러의 점수가 같아 비기는 상황하드 핸드(Hard Hand)
: 에이스가 없거나 에이스를 1로만 계산해야 하는 핸드
인터페이스에 상수를 두어도 될까?
CardGenerator
는 카드를 뽑는 방법을 정의하는 인터페이스이다. 카드 자체를 생성하는 코드는 모든 하위 구현체에 동일하므로, DECK이라는 카드 집합을 상수로 두었다.
AS IS
public interface CardGenerator {
List<Card> DECK = Arrays.stream(Shape.values())
.flatMap(shape -> Arrays.stream(Denomination.values())
.map(denomination -> new Card(shape, denomination)))
.toList(); // 인터페이스의 상수
Card pickCard();
}
effective java item 22
에서, "상수 인터페이스
를 사용하지 말라" 라고 한다. 그런데 상수를 정의하기 위한 목적이 아닌, 구현체에서 사용되는 공통 상수 를 인터페이스에 두는 것은 괜찮을까? 관련해서 리뷰어에게 물어보았다.
인터페이스를 행위를 정의하는 목적 이외에 추가 목적으로 사용한다면 인터페이스의 취지에 맞지 않다고 하셨다.
생각해보니 상수에 인터페이스를 정의하면 인터페이스를 구현한 모든 클래스에서 인터페이스에 정의한 상수에 접근하게 되어 네임스페이스가 오염된다. 또한 인터페이스의 정의에도 맞지 않다.
TO BE
public interface DeckGenerator {
Deque<Card> makeDeck();
}
public final class ShuffleDeckGenerator implements DeckGenerator {
private static final List<Card> INITIAL_CARDS; // 구현체에 상수를 두기
static {
INITIAL_CARDS = Arrays.stream(Suit.values())
.flatMap(suit -> Arrays.stream(Denomination.values())
.map(cardScore -> new Card(suit, cardScore)))
.toList();
}
@Override
public Deque<Card> makeDeck() {
final List<Card> cards = new ArrayList<>(INITIAL_CARDS);
Collections.shuffle(cards);
return new ArrayDeque<>(cards);
}
}
결론
- 인터페이스에 상수를 정의하지 말자. 인터페이스는 단순히 행위를 정의하는 목적으로만 사용하자.
this로 객체 넘기는 방식을 지양해야하는가?
Dealer.java
public ProfitResult makeDealerWinningResult(final Map<Player, Hand> playerScores) {
final Map<Player, ResultStatus> result = playerScores.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey,
entry -> calculateResultStatus(entry.getValue()),
(e1, e2) -> e1, LinkedHashMap::new));
return new ProfitResult(this, result);
}
딜러와 플레이어들의 수익 결과를 ProfitResult
의 생성자에서 만든다.
따라서 dealer
의 메서드를 호출하기 위해 ProfitResult
의 파라미터에 dealer
자신인 this
를 넘기게 되었다.
만약 dealer
내부에서 수익을 계산한다면 this
로 자신을 넘기지 않아도 된다. 하지만 그렇게 하지 않은 이유는 딜러 내부에 복잡한 수익 계산 로직이 들어가는 것이 책임 과다 문제, 즉 클래스 크기가 너무 커지는 문제가 발생될 수 있다고 생각하기 때문이다.
this를 넘기면 호출 권한을 해당 객체에게 넘기는 것인데, 이 방식이 괜찮은 방식인지 리뷰어에게 물어보았다.
생성자에 로직을 포함하는 것이 좋은가?
우선 생성자에서 수익 결과를 계산하는 부분에 대해 이야기하셨다.
public final class ProfitResult {
private static final double AVERAGE_PROFIT_RATE = 1.0;
private static final double BLACKJACK_PROFIT_RATE = 1.5;
private final Map<Gamer, Integer> result;
public ProfitResult(final Dealer dealer, final Map<Player, ResultStatus> winningResult) {
this.result = calculateProfit(dealer, winningResult);
}
생성자에 로직을 포함한다면 생성자의 책임을 넘어서는 일은 아닌지 이야기하셨다.
생각해보면, 생성자에 로직을 포함하면 생성 외의 책임을 구현을 통해 확인해보아야하고, 테스트시에도 생성 결과를 예상하기 어렵다.
따라서 생성자는 단순히 DI
하는 역할만 수행하고, 로직을 포함하고 싶다면 정적 팩토리 메서드나 외부 객체를 사용하기로 하였다.
TO BE
public final class ProfitResult {
private final Map<Participant, BigDecimal> profitByParticipant;
public ProfitResult(final Map<Participant, BigDecimal> profitByParticipant) {
this.profitByParticipant = profitByParticipant;
}
public static ProfitResult from(final Dealer dealer, final Players players) {
final Map<Participant, BigDecimal> profit = initializeProfit(dealer, players);
for (Player player : players.getPlayers()) {
calculateEachProfit(dealer, player, profit);
}
return new ProfitResult(profit);
}
private static void calculateEachProfit(final Dealer dealer, final Player player,
final Map<Participant, BigDecimal> profits) {
final BigDecimal profit = player.calculateProfit(dealer);
profits.merge(dealer, profit.negate(), BigDecimal::add);
profits.merge(player, profit, BigDecimal::add);
}
this로 넘기는 것
this
로 넘기는 것에 대해서는 꼭 지양하거나, 지향할 영역은 아닌 것 같다고 이야기해주셨다.- 객체의 책임이 커지면 다른 객체로 책임을 분리해야하니
this
로 객체를 넘겨줄 수 밖에 없는 상황이 발생할 수 있다. 객체끼리 결합도가 너무 높아지는 문제가 있을 수 있겠지만, 그렇다고 과도한 책임을 가지는 것이 더 좋지 않기 때문이다.
this로 넘기기보다 데이터를 넘겨주는 것이 더 좋긴 하다.
- 우선, 책임을 분리하는 것을 먼저 생각해보라고 하셨다.
책임 분리를 위해 새로운 객체 생성
기존에는 딜러에서 승패 로직을 계산하고, 이를 수익 계산 객체로 생성하는 책임을 모두 가졌다. 책임 분리를 위해 승패 로직을 계산하는 새로운 객체
를 만들고, 이를 수익 계산 객체
로 넘기는 방식으로 리팩토링하였다.
public class PlayerScoreResult {
private final Map<Player, Hand> playerHands;
public PlayerScoreResult(final Map<Player, Hand> playerHands) {
this.playerHands = playerHands;
}
public Map<Player, ResultStatus> calculateScoreResult(final Dealer dealer) {
return playerHands.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey,
entry -> determinePlayerResult(dealer.showAllCards(), entry.getValue()),
(e1, e2) -> e1, LinkedHashMap::new));
}
...
상속 + 인터페이스 구현
추상 골격 클래스
public final class Dealer extends Participant implements GameRule {
private static final String NICKNAME = "딜러";
private static final int DEALER_THRESHOLD = 16;
private static final int SPREAD_CARD_SIZE = 2;
public Dealer(final Hand hand) {
super(hand);
}
public Dealer() {
this(new Hand(new ArrayList<>()));
}
@Override
public Hand showInitialCards() {
return new Hand(List.of(hand.getFirstCard()));
}
@Override
public boolean canHit() {
int score = hand.calculateWithHardHand();
return score <= DEALER_THRESHOLD;
}
step1
에서는 플레이어와 딜러의 공통 상위 클래스(Participant
)를 정의하여 상속을 통해 중복 코드를 줄였다. 즉 Participant
추상 클래스는 게임 참여자가 가지는 공통 로직을 정의하였다.
step2
에서는 추상 골격 클래스
를 이용하여 상속
뿐 아니라 인터페이스
를 구현하도록 설계했다.
"effective java item 20. 추상 클래스보다는 인터페이스를 우선하라" 를 참고하였다.
딜러가 게임 참여자 의 역할을 할 때는 Participant
로부터 상속받은 멤버들을 사용하고, 게임 진행자일때는 인터페이스(GameRule)
로 정의된 메서드를 사용하였다. 이를 통해 코드를 재사용하면서 유연하게 구현할 수 있었다.
추상 골격 클래스 사용에 대해 오버 엔지니어링
인지, 좋은 설계
인지 리뷰어의 의견을 물어보았다.
추상 골격 클래스가 단순히 상속 + 인터페이스 구현을 뜻하는 것은 아니라고 이야기하셨다.
추상 골격 클래스란?
인터페이스의 기본적인 로직을 제공하면서, 일부 동작은 서브클래스가 구현하도록 강제
하는 방식- 인터페이스를 부분적으로 구현하여 뼈대를 정의하고, 서브 클래스에서 메서드를 구현할 수 있게 하여 구현 부담을 줄이고 코드를 재사용하는 패턴
Dealer는 추상 골격 클래스가 아니다
- 인터페이스와 상위 클래스가 다른 책임을 가지므로 추상 골격 클래스 용어 개념과 다르다. 추상 메서드를 구현하지 않고 남겨둔 것도 아니다.
- 단순히 인터페이스 + 상속이면 추상 골격 클래스라고 생각했다고 반성하게 되었다.😂 덕분에 정확하게 해당 개념을 공부할 수 있게 되었다.
인터페이스로 역할을 분리하는 것 (ISP)
public interface Participant {
Hand showInitialCards();
boolean canHit();
void changeState(State inputState);
Hand showAllCards();
String getNickname();
}
public interface GameRule {
ProfitResult calculateProfit(Players players);
}
public final class Player implements Participant {
Player
는 참가자 역할만 수행한다.
public final class Dealer implements Participant, GameRule {
Dealer
는 참가자와 게임 관리 역할을 수행한다.
현재 구현하는 블랙잭 게임은 요구사항이 고정되어 있어 굳이 인터페이스를 역할로 쪼갤 필요가 없지만, 실무에서는 확장이 닫혀있음을 보장하지 못한다.
따라서 역할 기반 인터페이스는 명세를 명확하게 파악하고, 변경점이 명확하여 유지보수면에서 더 좋다. 또한 테스트시에도 편하다.
결론
- 블랙잭이라는 도메인에서는 오버엔지니어링이 맞지만, 다른 도메인이라면 좋은 설계일 수도 있다!
- 인터페이스로 역할 분리
- 장점 : 유연성, 확장성, 테스트 용이성
- 단점 : 복잡도 증가
캐싱 사용
캐싱을 하면 셔플된 덱을 생성할 때마다 모든 카드를 생성하지 않아도 되어 좋다.
public final class ShuffleDeckGenerator implements DeckGenerator {
private static final List<Card> INITIAL_CARDS;
static {
INITIAL_CARDS = Arrays.stream(Suit.values())
.flatMap(suit -> Arrays.stream(Denomination.values())
.map(cardScore -> new Card(suit, cardScore)))
.toList();
}
@Override
public Deque<Card> makeDeck() {
final List<Card> cards = new ArrayList<>(INITIAL_CARDS);
Collections.shuffle(cards);
return new ArrayDeque<>(cards);
}
}
참고 ) static
블록
static
블록은 상수 선언과 같이 클래스 로딩시에 한번만 수행된다. 인스턴스 생성시에 매번 수행되지 않는다.- 상수 선언과 달리 복잡한 초기화 로직을 정의할 수 있다.
돈을 다룰 때 BigDecimal 사용
돈은 오차가 발생하면 안되므로 BigDecimal을
사용하는 것이 좋다.
private BigDecimal compareByScore(final BigDecimal bettingAmount, final Dealer dealer) {
int playerScore = calculateScore();
int dealerScore = dealer.calculateScore();
if (playerScore == dealerScore) {
return BigDecimal.ZERO;
}
if (playerScore < dealerScore) {
return bettingAmount.negate();
}
상태 패턴으로 리팩토링
상태 패턴을 이용해서 게임 결과를 처리하는 분기문을 제거하고, 상태별 로직을 객체로 분리할 수 있다.
AS IS
PlayerHand.java
- 플레이어와 딜러의 상태를 각각 따지는 분기문이 많았다.
private ResultStatus determinePlayerResult(final Hand dealerHand, final Hand playerHand) {
if (dealerHand.isBlackjack() && !playerHand.isBlackjack()) {
return LOSE;
}
if (!dealerHand.isBlackjack() && playerHand.isBlackjack()) {
return BLACKJACK;
}
return determineRegularGameResult(dealerHand, playerHand);
}
private ResultStatus determineRegularGameResult(final Hand dealerHand, final Hand playerHand) {
int dealerScore = dealerHand.calculateResult();
int playerScore = playerHand.calculateResult();
if (dealerScore <= BURST_THRESHOLD) {
return determineResultWhenDealerNotBusted(dealerScore, playerScore);
}
return determineResultWhenDealerBusted(playerScore);
}
private ResultStatus determineResultWhenDealerNotBusted(final int dealerScore, final int playerScore) {
if (playerScore <= BURST_THRESHOLD) {
return determineResultWhenPlayerNotBusted(dealerScore, playerScore);
}
return LOSE;
}
TO BE
DealerRunningState.java
public final class DealerRunningState extends RunningState {
private static final int DEALER_THRESHOLD = 16;
private DealerRunningState(final Hand hand) {
super(hand);
}
public static State initialState(final Hand givenHand) {
if (givenHand.isBlackjack()) {
return new BlackjackState(givenHand);
}
if (isStay(givenHand)) {
return new StayState(givenHand);
}
return new DealerRunningState(givenHand);
}
private static boolean isStay(final Hand givenHand) {
return givenHand.calculateScore() > DEALER_THRESHOLD;
}
@Override
public State receiveCards(final Hand givenHand) {
hand.addAll(givenHand);
if (hand.isBust()) {
return new BustState(hand);
}
if (isStay(hand)) {
return new StayState(hand);
}
return this;
}
}
Dealer.java
public final class Dealer implements Participant, GameRule {
private static final String NICKNAME = "딜러";
private State state;
public Dealer(final Hand hand) {
this.state = DealerRunningState.initialState(hand);
}
public static Dealer initializeDealer(final Deck deck, final int count) {
Hand dealerHand = deck.drawCardsByCount(count);
return new Dealer(dealerHand);
}
@Override
public Hand showInitialCards() {
return new Hand(List.of(state.cards().getFirstCard()));
}
@Override
public boolean canHit() {
return state.isNotFinished();
}
고려해야할 것
- 상태(
State
)는 딜러와 플레이어 모두 가지므로, 딜러나 플레이어와만 관련있는 코드는 제거하는 것이 좋다.dealer
가stay
인지 여부는Dealer
가 두도록 수정- 수익 계산 로직
Player
로 이동
PR
https://github.com/woowacourse/java-blackjack/pull/805
[1단계 - 블랙잭] 밍트(김명지) 미션 제출합니다. by Starlight258 · Pull Request #805 · woowacourse/java-blackja
안녕하세요, 밍트입니다. 반갑습니다! 😊 체크 리스트 미션의 필수 요구사항을 모두 구현했나요? Gradle test를 실행했을 때, 모든 테스트가 정상적으로 통과했나요? 애플리케이션이 정상적으로
github.com
https://github.com/woowacourse/java-blackjack/pull/917
[2단계 - 블랙잭 베팅] 밍트(김명지) 미션 제출합니다. by Starlight258 · Pull Request #917 · woowacourse/java-
로키 안녕하세요! 밍트입니다! 😊 블랙잭 2단계도 잘 부탁드립니다! 🙇♀️ 체크 리스트 미션의 필수 요구사항을 모두 구현했나요? Gradle test를 실행했을 때, 모든 테스트가 정상적으로 통과
github.com