객체지향 좀 더 이해하기 - 블랙잭 게임 구현 (3)

Java·2016.11.27 23:56


2-3. Dealer 구현

다음 단계로 Dealer를 구현해보겠습니다.
Dealer의 역할은 아래와 같습니다.

  • 추가로 카드를 받는다.
  • 단, 2카드의 합계 점수가 16점 이하이면 반드시 1장을 추가로 뽑고, 17점 이상이면 받을 수 없다.
  • 뽑은 카드를 소유한다.
  • 카드를 오픈한다.

이렇게 될 수 있었던 이유는 게임의 승패를 판단하는 것은 Rule 객체가, 카드를 뽑는 것은 카드덱 객체가 맡았기 때문입니다.
그럼 위 역할들만 구현해보겠습니다.
Dealer에는 Gamer와 달리 16점 이하일 경우에만 추가로 카드를 받을 수 있다는 제한이 있습니다.
이를 구현하기 위해 각 카드별로 포인트를 가질 수 있도록 하는 것이 좋습니다.
해당 기능은 카드를 뽑을때마다 계산할 수도 있지만, 처음 CardDeck에서 카드를 생성할 때 저장하는 것이 반복적인 작업이 줄어들기 때문에 CardDeck 부분을 수정하도록 하겠습니다.

CardDeck.java

    private List<Card> generateCards() {
        List<Card> cards = new LinkedList<>();

        for(String pattern : PATTERNS){
            for(int i=1; i<=CARD_COUNT; i++) {
                String denomination = this.numberToDenomination(i);
                int point = this.numberToPoint(i);
                Card card = new Card(pattern, denomination, point);
                cards.add(card);
            }
        }

        return cards;
    }

    private int numberToPoint(int number) {
        if(number >= 11){
            return 10;
        }

        return number;
    }    

generateCards 메소드에 Card 인스턴스의 생성 인자로 point가 추가 되며,
numberToPoint 메소드에서 해당 Card의 point값을 계산하는 역할을 담당하도록 작성하였습니다.
이렇게 작성될 경우 CardDeck이 생성될 때 52개 카드의 point도 자동으로 할당이 될 것입니다.

Card.java

    private int point;
    public Card(String pattern, String denomination, int point) {
        this.pattern = pattern;
        this.denomination = denomination;
        this.point = point;
    }

Card 클래스에는 point 생성자 인자값이 추가되는것 외에는 변경사항이 없습니다.

여기까지 작성후, CardDeck 클래스를 보겠습니다.
단지 52개의 카드만 생성하면 되는 CardDeck에 너무 많은 private 메소드가 있다는 생각이 들지 않으신가요?
하나의 클래스에 private 메소드가 많아지면 객체 설계를 다시 고민해봐야 하는 신호로 보시면 됩니다.
CardDeck의 책임에 대해 다시 생각해보면,
CardDeck은 단지 서로 다른 52개의 카드만 생성하면 됩니다.
즉, 반복문의 숫자가 변함에 따라 어떤 끗수가 할당 되는지에 대해서 전혀 책임이 없습니다.
이는 Card 객체의 책임입니다.
그래서 이를 분리하도록 하겠습니다.

Card.java

    public Card(String pattern, int index) {
        this.pattern = pattern;
        this.denomination = this.numberToDenomination(index);
        this.point = this.numberToPoint(index);
    }

    private String numberToDenomination(int number){

        if(number == 1){
            return "A";
        }else if(number == 11){
            return "J";
        }else if(number == 12){
            return "Q";
        }else if(number == 13){
            return "K";
        }

        return String.valueOf(number);
    }

    private int numberToPoint(int number) {
        if(number >= 11){
            return 10;
        }

        return number;
    }

CardDeck의 numberToDenomination와 numberToPoint를 Card 클래스로 이동하였습니다.
그리고 생성자를 수정하여 반복문의 index가 들어오면 denomination과 point를 계산하도록 수정하였습니다.

CardDeck.java

    private List<Card> generateCards() {
        List<Card> cards = new LinkedList<>();

        for(String pattern : PATTERNS){
            for(int i=1; i<=CARD_COUNT; i++) {
                Card card = new Card(pattern, i);
                cards.add(card);
            }
        }

        return cards;
    }

Card 객체로 역할을 분리했기 때문에 CardDeck은 반복문으로 Card 인스턴스를 생성만 하면 되도록 코드가 간결해졌습니다.
Dealer 코드 작성의 밑거름은 모두 작성되었으니, 본격적으로 Dealer의 코드를 구현해보겠습니다.

Dealer.java

public class Dealer {
    private List<Card> cards;

    private static final int CAN_RECEIVE_POINT = 16;

    public Dealer() {
        cards = new ArrayList<>();
    }

    public void receiveCard(Card card) {
        if(this.isReceiveCard()){
            this.cards.add(card);
            this.showCards();
        }else{
            System.out.println("카드의 총 합이 17이상입니다. 더이상 카드를 받을 수 없습니다.");
        }
    }

    private boolean isReceiveCard(){
        return getPointSum() <= CAN_RECEIVE_POINT;
    }

    private int getPointSum(){
        int sum = 0;
        for(Card card : cards) {
            sum += card.getPoint();
        }

        return sum;
    }

    public void showCards(){
        StringBuilder sb = new StringBuilder();
        sb.append("현재 보유 카드 목록 \n");

        for(Card card : cards){
            sb.append(card.toString());
            sb.append("\n");
        }

        System.out.println(sb.toString());
    }

    public List<Card> openCards(){
        return this.cards;
    }
}

Dealer의 가장 큰 특징인 receiveCard 메소드를 구현하기 위해 isReceiveCard메소드와 getPointSum메소드를 추가하였습니다.
여기서 특이하다고 느끼실 만한 것은 isReceiveCard 메소드 일것 같습니다.
단순히 한줄의 코드, boolean의 리턴값밖에 없는데도 메소드를 만들어야 하나? 라는 의문이 있으실것 같습니다.
결론은 만드는 것이 낫다입니다. 매직넘버와 비슷한 케이스로 보시면 됩니다.
if(getPointSum() <= CAN_RECEIVE_POINT) 이라고 하면 이 조건문이 정확히 어떤 일을 하는지는 추측할 수 밖에 없습니다.
하지만 if(this.isReceiveCard()) 로 하게 되면 아! 카드를 받을 수 있는 조건은 총 포인트의 합이 16이하인 경우이고 이때는 카드를 받을 수 있구나 라고 확신할 수 있습니다.
이게 중요합니다.
결국은 누가 봐도 이 코드가 무엇을 하는지 명확히 표현하는 것이 중요합니다.
그 일은 주석이 하는 것이 아닙니다. 의도가 명확한 코드와 변수명, 메소드명이 해야하는 것입니다.

Dealer의 구현이 끝났으니, Dealer가 필요한 Game 클래스를 수정해보겠습니다.

    private void playingPhase(Scanner sc, CardDeck cardDeck, Gamer gamer, Dealer dealer) {
        String gamerInput, dealerInput;
        boolean isGamerTurn = false,
                isDealerTurn = false;

        while(true){
            System.out.println("카드를 뽑겠습니까? 종료를 원하시면 0을 입력하세요.");
            gamerInput = sc.nextLine();

            if("0".equals(gamerInput)) {
                isGamerTurn = true;
            }else{
                Card card = cardDeck.draw();
                gamer.receiveCard(card);
            }

            System.out.println("카드를 뽑겠습니까? 종료를 원하시면 0을 입력하세요.");
            dealerInput = sc.nextLine();

            if("0".equals(dealerInput)) {
                isDealerTurn = true;
            }else{
                Card card = cardDeck.draw();
                dealer.receiveCard(card);
            }

            if(isGamerTurn && isDealerTurn){
                break;
            }
        }
    }

    private void initPhase(CardDeck cardDeck, Gamer gamer, Dealer dealer){
        System.out.println("처음 2장의 카드를 각자 뽑겠습니다.");
        for(int i = 0; i< INIT_RECEIVE_CARD_COUNT; i++) {
            Card card = cardDeck.draw();
            gamer.receiveCard(card);

            Card card2 = cardDeck.draw();
            dealer.receiveCard(card2);
        }
    }

코드를 보시면 불편해 보이는 코드가 대거 보이실것 같습니다.
다음 시간엔 이 코드를 리팩토링 해보겠습니다.







IntelliJ & 안드로이드 스튜디오의 기본기를 배우고 싶다면 아래 영상을 참고해보세요!

Posted by 창천향로 창천향로

태그