객체 지향 설계 연습하기 - 블랙잭 (3)

객체 지향 설계 연습하기 - 블랙잭 (1)

github source code

0. 들어가며

  • 업무에 Java를 사용하고 있지만, 깊은 이해도가 부족하다는걸 절감.
  • 단순 객체 생성 및 비즈니스 로직 구현에만 매달리고 있음. 회의감이 듦.
  • 신규 개발 뿐만 아니라 유지 보수 및 리팩토링시 객체 지향의 묘미를 살려보고자 함
  • 객체 지향적 시야와 사고는 연습뿐이라는 것을 여러 커뮤니티에서 수집
  • 객체 지향 설계 연습을 통해 객체 지향적 시야와 이해력을 높이고지 함

1. 리팩토링

  • Card
  • Suit와 Denomination의 구현체로 변경
  • 추상화 개념에서 한개의 카드는 한 개의 무늬와 끗수를 가짐
  • 체크 메소드를 통해 카드를 확인하는 행동을 구현
package personal.cjh.BlackJack.card;

import org.apache.commons.lang3.tuple.ImmutablePair;

/**
 * 카드 클래스
 * 카드 구현체 입니다.
 * 카드는 추상 개념이 아니라 실제 구현된 구현물체라서 클래스로 변경했습니다.
 */
public class Card {
  private Suit suit;
  private Denomination denomination;

  /**
   * 새로운 카드를 만들때 호출되는 생성자
   *
   * @param suit 한벌 중 한개의 모양
   * @param denomination 끗수
   */
  public Card(Suit suit, Denomination denomination) {
    this.suit = suit;
    this.denomination = denomination;
  }

  /**
   * 카드 인스턴스의 모양과 끗수를 리턴
   * 카드를 확인하는 행동을 check 메소드로 추상화
   *
   * @return 변경 불가능한 한쌍으로 리턴합니다.
   */
  public ImmutablePair<Suit, Denomination> check() {
    return new ImmutablePair<Suit, Denomination>(suit, denomination);
  }
}
  • Denomination
  • 변동 없음
package personal.cjh.BlackJack.card;

import org.apache.commons.lang3.tuple.ImmutablePair;

/**
 * 카드 클래스
 * 카드 구현체 입니다.
 * 카드는 추상 개념이 아니라 실제 구현된 구현물체라서 클래스로 변경했습니다.
 */
public class Card {
  private Suit suit;
  private Denomination denomination;

  /**
   * 새로운 카드를 만들때 호출되는 생성자
   *
   * @param suit 한벌 중 한개의 모양
   * @param denomination 끗수
   */
  public Card(Suit suit, Denomination denomination) {
    this.suit = suit;
    this.denomination = denomination;
  }

  /**
   * 카드 인스턴스의 모양과 끗수를 리턴
   * 카드를 확인하는 행동을 check 메소드로 추상화
   *
   * @return 변경 불가능한 한쌍으로 리턴합니다.
   */
  public ImmutablePair<Suit, Denomination> check() {
    return new ImmutablePair<Suit, Denomination>(suit, denomination);
  }
}
  • Suit
  • 변동 없음
package personal.cjh.BlackJack.card;

/**
 * The Enumeration Suit.
 * 한벌, 한세트를 뜻함
 * 스페이드, 다이아몬드, 클로버, 하트
 */
public enum Suit {
  /**
   * SPADE, DIAMOND, CLOVER, HEART
   */
  SPADE("SPADE"), DIAMOND("DIAMOND"), CLOVER("CLOVER"), HEART("HEART");

  private String Property;

  Suit(String property) {
    this.Property = property;
  }

  /**
   * 속성을 출력합니다.
   *
   * @return the property
   */
  public String getProperty() {
    return Property;
  }
}
  • Deck
  • 덱은 Card의 변경에따라 다소 변경이 있음
  • Card 객체를 직접 사용함에 따라 코드가 직관적으로 바뀜
  • DrawCard 메소드는 카드 객체를 반환하고, Card를 받은 유저가 check 메소드 호출을 통해 끗수와 무늬를 확인
package personal.cjh.BlackJack.deck;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import personal.cjh.BlackJack.card.Card;
import personal.cjh.BlackJack.card.Denomination;
import personal.cjh.BlackJack.card.Suit;

/**
 * 덱 클래스 입니다.
 * 덱 또한 여러장의 카드를 모아논 것이기 때문에 클래스를 이용했습니다.
 */
public class Deck {
  private final List<Card> deck = new ArrayList<>();
  private final Random random = new Random();

  /**
   * 덱을 생성할때 새로운 카드를 deck list에 넣습니다.
   * 덱은 각각의 덱별로 블랙잭 카드 개수인 52장씩 생성됩니다.
   */
  public Deck() {
    for (int i = 0; i < Suit.values().length; i++) {
      for (int j = 0; j < Denomination.values().length; j++) {
        deck.add(new Card(Suit.values()[i], Denomination.values()[j]));
      }
    }
  }

  /**
   * 덱에서 카드를 드로우 합니다.
   * 실제로 드로우 하는 주최는 딜러가 될것이며, 유저는 딜러에게 요청만 하게 됩니다.
   * Deck 리스트에서 랜덤으로 하나 출력 후 해당 카드는 삭제 합니다.
   *
   * @return 카드 객체로 반환
   */
  public Card drawCard() {
    int index = random.nextInt(deck.size());
    Card card = deck.get(index);
    deck.remove(index);
    return card;
  }

  /**
   * 덱 리스트 변수에 접근하기 위한 메소드
   *
   * @return 덱 리스트를 반환합니다
   */
  public List<Card> getDeck() {
    return deck;
  }
}

테스트

package personal.cjh.BlackJack.deck;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;
import personal.cjh.BlackJack.card.Card;
import personal.cjh.BlackJack.card.Denomination;
import personal.cjh.BlackJack.card.Suit;

public class DeckTest {
  @Test
  public void 인스턴스로_생성된_덱의_유효성_검사() {
    // GIVEN
    Deck deck = new Deck();

    // WHEN
    boolean nullPointCheck = deck == null;
    boolean instanceCheck = deck instanceof Deck;
    boolean deckSizeCheck = deck.getDeck().size() == deck.getDeck().size();

    deck.drawCard();

    // THEN
    assertFalse(nullPointCheck);
    assertTrue(instanceCheck);
    assertTrue(deckSizeCheck);
  }

  @Test
  public void 덱에서_카드를_뽑았을때_덱의_상태변화를_검사한다() {
    // GIVEN
    Deck deck = new Deck();

    // WHEN
    int beforeDeckSize = deck.getDeck().size();
    Card card = deck.drawCard();
    int afterDeckSize = deck.getDeck().size();

    // THEN
    assertFalse(beforeDeckSize == afterDeckSize);
    assertTrue(beforeDeckSize == 52);
    assertTrue(afterDeckSize == 51);
  }

  @Test
  public void 덱에서_카드를_뽑았을때_카드의_유효성을_검사한다() {
    // GIVEN
    Deck deck = new Deck();

    // WHEN
    Card card = deck.drawCard();

    // THEN
    assertTrue(card != null);
    assertThat(card, instanceOf(Card.class));
    assertThat(card.check(), instanceOf(ImmutablePair.class));
    assertThat(card.check().left, instanceOf(Suit.class));
    assertThat(card.check().right, instanceOf(Denomination.class));
  }

  @Test
  public void 덱이_싱글톤인지_검사한다() {
    // GIVEN
    Deck deck = new Deck();
    Deck deck2 = new Deck();

    // WHEN
    boolean beforeDrawCheck = (deck.getDeck() == deck.getDeck());
    deck.drawCard();
    boolean afterDrawCheck = (deck.getDeck() == deck.getDeck());
    boolean isEqual = (deck.getDeck() == deck2.getDeck());

    // THEN
    assertTrue(beforeDrawCheck);
    assertTrue(afterDrawCheck);
    assertFalse(isEqual);
  }

  @Test
  public void getCard_함수를_사용했을때_덱안의_카드가_정상적으로_줄어드는지_검사() {
    // GIVEN
    Deck deck = new Deck();

    // WHEN
    boolean initSizeEqual = (deck.getDeck().size() == 52);
    Card card = deck.drawCard();

    // THEN
    assertThat(card.check().getLeft(), instanceOf(Suit.class));
    assertThat(card.check().getRight(), instanceOf(Denomination.class));
    assertThat(deck.getDeck().size(), is(51));

    int count = 51;
    while (deck.getDeck().size() != 0) {
      Card cd = deck.drawCard();
      printCard(cd.check(), count);
      assertThat(deck.getDeck().size(), is(--count));
    }
  }

  public void printCard(Pair<Suit, Denomination> card, int count) {
    StringBuilder sb = new StringBuilder();
    sb.append("COUNT : ");
    sb.append(count);
    sb.append(" Suit : ");
    sb.append(card.getLeft());
    sb.append(" Suit Prop : ");
    sb.append(card.getLeft().getProperty());
    sb.append(" Denomination : ");
    sb.append(card.getRight());
    sb.append(" Denomination Prop : ");
    sb.append(card.getRight().getProperty());
    System.out.println(sb.toString());
    sb.setLength(0);
  }
}

마무리

  • 카드쪽에서 고민이 많았는데 abstract, interface 대신 Suit와 Denomination을 구현한 클래스를 사용함으로써 전체적인 코드가 직관적으로 바뀜