블랙잭 게임 - 3 핵심 로직(2)

Kim Dong Kyun·2023년 9월 20일
2
post-thumbnail

지난 이야기들

Github 주소 : 코드 확인은 여기서!

저번 시간 남은 할 일 체크하면서

위와 같은 말을 남겼는데, 위 방법이 아닌 다른 방법으로 구현했다. (상속을 이용하는 형태)

++ User, Users 라는 이름을 쓸 수 없다 ㅠ(아쉽게도, 이미 그 전 prac들에서 클래스가 정의되어 있어서 조금 어색한 이름을 차용했다. 클래스, 매서드, 변수명은 계속 고민중...)


유저의 상태를 체크하는 Enum

CardSumStatus.java

public enum CardSumStatus {
    UNDER, BLACK_JACK, EXCEEDED,
    DEALER_MUST_PICK, DEALER_MUST_STAY;

    private static final Integer BLACKJACK_COUNT = 21;
    private static final Integer DEALER_STANDARD = 16;

    public static CardSumStatus getNumberStatusForPlayer(int cardSum){
        if (cardSum > BLACKJACK_COUNT) return EXCEEDED;
        if (cardSum < BLACKJACK_COUNT) return  UNDER;
        return BLACK_JACK;
    }

    public static CardSumStatus getNumberStatusForDealer(int cardSum){
        if (getNumberStatusForPlayer(cardSum) == BLACK_JACK) return BLACK_JACK;
        if (cardSum <= DEALER_STANDARD) return DEALER_MUST_PICK;
        return  DEALER_MUST_STAY;
    }
}

유저는
1. UNDER - 21 이하
2. BLACK_JACK - 21
3. EXCEEDED - 21 초과

의 상태를 가지며, 딜러는 특수한 조건

("딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.")

이 있기 때문에 다른 해당 조건에 맞춰 주었다.

위 Enum을 User 쪽에선 어떻게 활용해야 할까?


GameUser (전 Gamer) - 유저 추상 클래스에 abstract 매서드 추가

abstract CardSumStatus statusCheck();

위와 같은 매서드를 유저의 상위 추상에 정의했다. 그리고 딜러와 유저는 이 매서드를 오버라이드해서, 자신만의 상태 체크 매서드를 가지면 될 것이다.

그리고 해당 상태 체크 매서드는 게임의 진행 중 카드가 부여되고, 패를 체크하는 시점에 호출되어

"유저" 자신이 어떤 상태를 가지는지 스스로 표현 할 수 있다. 그리고, 이 모든 과정을 지켜보는 Controller 측에서는 유저에게 "표현하라" 고 명령하면 된다.

Dealer.java

public class Dealer extends GameUser {
    private static final String DEALER_NAME = "Dealer";

    public Dealer() { // 딜러는 고정된 이름을 가지도록 의도
        super(DEALER_NAME);
    }
    @Override
    public CardSumStatus statusCheck() {
        return CardSumStatus.getNumberStatusForDealer(this.getCardSum());
    }
}
  • 딜러가 가질 매서드이다. (ForDealer)

  • GameUser및 하위 클래스 객체는 스스로의 카드 총합을 계산하는 구상 매서드 getCardSum() 을 가진다.

  • 해당 구상 매서드를 이용해서 객체의 상태를 체크하는 로직으로 구성했다.

  • Enum 클래스에서 static하게 바로 호출하는 것 보다는, Dealer 에게 그 책임을 주는 것이 맞다고 생각해서 위와 같은 형태로 만들었다.

Player.java

public class Player extends GameUser {
    public Player(String name) {
        super(name);
    }

    @Override
    public CardSumStatus statusCheck() {
        return CardSumStatus.getNumberStatusForPlayer(this.getCardSum());
    }
}
  • 마찬가지로 간단한 형태로 구현했다. (ForPlayer)

잠깐, 왜 익명클래스를 쓰지 않았는지?

익명 클래스?

ex) 조건에 따라 합산하는 부분을, @FunctionalInterface 를 이용해서 해결하는 코드.

더 자세한 건 이전 글 링크!

@FunctionalInterface
public interface SumWithCondition {
    boolean sumCondition(int number);
}

...
class foo {
...
    public static void runThread() {
        new Thread(() -> System.out.println("Hello from thread")).start();
    }

    public static int sumAll(List<Integer> numbers) {
        return sumByCondition(numbers, number -> true);
    }

    public static int sumAllEven(List<Integer> numbers) {
        return sumByCondition(numbers, number -> number % 2 == 0);
    }

    public static int sumAllOverThree(List<Integer> numbers) {
        return sumByCondition(numbers, number -> number > 3);
    }

    public static int sumByCondition(List<Integer> numbers, SumWithCondition sumWithCondition){
        return numbers.stream().filter(sumWithCondition::sumCondition).reduce(0, Integer::sum);
    }

그럼, 이걸 사용하지 않은 이유는?

  • 해당 방식을 사용하려면

1.매서드들이 같은 클래스 내에 존재하고, 따라서 그 안에서 자유롭게 호출이 가능하거나,
2.public static 매서드라서 여러 클래스에서 접근 가능한 매서드라는 전제 조건이 필요하다.

  • 그러나 public static 매서드가 단순한 유틸기능을 가지는 것이 아닌, 객체를 리턴하는 일을 수행하는 것은 좋지 못하다고 판단했다

(리턴하는 녀석이 Enum, 즉 싱글턴 인스턴스여서 큰 문제가 없다고 해도, 어쨋든 해당 로직이 존재하는 "특정 클래스"에 대한 결합이 높아진다고 생각했다)

  • 따라서 현재의 하위 클래스별로(User 하위들) @Override 하는 방식을 채택

일급 컬렉션 Players

Players.java

public class Players {
    private final List<GameUser> users;

    public Players(List<GameUser> users) {
        this.users = users;
    }

    public int size() {
        return users.size();
    }

    public void dealCards(Deck cards) {
        this.users.forEach(gamer -> gamer.pickCard(cards.popCard()));
    }

    public List<String> getPlayersNames(){
        return this.users.stream().map(GameUser::getName).collect(Collectors.toList());
    }

    public GameUser findUserByUsername(String username){
        return users.stream().filter(user -> user.getName().equals(username)).findAny().orElseThrow(
                () -> new IllegalArgumentException("해당하는 유저가 없다.")
        );
    }

    public List<Card> getCardsInHandByUsername(String username){
        GameUser user = this.findUserByUsername(username);
        return user.getCardsInHand();
    }
}

불변, 일급 컬렉션이다.

  • 이 컬렉션이 해야 할 일은
    유저마다 어떤 손패를 가지고 있는지 알려주는 일이다.

컨트롤러와 이 컬렉션의 상호작용을 현재는 다음과 같이 구상했다.

  1. 컨트롤러 측에서는 Players를 가지고 있으며, 컨트롤러 측에서는 이 녀석들의 이름을 받아서 사용한다. (직접 List 컬렉션을 넘겨주지 않는다.)

  2. 컨트롤러측에서는 넘겨 받은 이름을 활용해서, 이름마다 손패가 어떤지 물어본다. 그 후 OutputView 에게 결과를 넘겨준다.


컨트롤러의 초안

BlackJackController.java

public class BlackjackController {
    private final Deck deck;
    private final Players players;

    public BlackjackController(Deck deck, Players players) {
        this.deck = deck;
        this.players = players;
    }

    public void createDeck() {
        this.deck.createCardDeck();
        this.deck.shuffleDeck();
    }

    public Card popCard(){
        return this.deck.popCard();
    }

    public int totalPlayerSize(){
        return this.players.size();
    }

    public void dealCards() {
        this.players.dealCards(this.deck);
    }
}
  • 이제껏 테스트하며 만들어온 그 모든 녀석들을 사용하는 녀석

  • 현재는 Input, OutputView 와의 연결은 없다. (먼저, 테스트 용이한 로직들부터 테스트하기 위함도 있고, 귀찮기도 했다...)

  • 대부분의 로직은 각자 객체들이 알아서 책임지고 실행한다. 컨트롤러는 실행 타이밍을 조율한다.


현재까지의 소감

작은 기능들을 모듈화, 조립해서 큰 녀석으로 만들어가는 과정의 재미를 조금 느껴본 것 같다.

내가 필요로 하는 로직들이 대부분 다른 곳에서 이미 구현되었거나, 이미 구현된 것을 바탕으로 조금만 더 손보면 만들 수 있었다.

특히 테스트가 굉장히 효율적인 수단이구나를 다시 한 번 느꼈다. 내가 직접 느낀 테스트의 장점은

  1. 갈아엎을 때 용기를 준다.
  • 완전히 갈아엎더라도 알아서 에러를 뱉던지, 실패를 해주니까 어떤 부분이 영향을 받았는지, 무엇이 문제인지 파악하기가 훠~~~얼씬 수월하다.
  1. 새로운 기능을 용감하게 만들 수 있다. (심지어 더 쉽게)
  • "테스트 가능한 로직"을 만들기 위해서는 필연적으로 매서드의 호흡이 짧아지고, (한 번에 하나만 검증해야 하므로) 자연스럽게 모듈화하기 좋은 코드를 (가용성이 높은?) 짜게 만드는 것 같다.
  1. 재미있다.
  • 즉각적으로 확인 가능한, 즉각적 보상이 오는 코드는 너무 재미있다.

글 내용에 대해서 어떤 것이든 자유롭게 피드백 부탁드립니다!

댓글 남겨주시면 더 좋구요~

0개의 댓글