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

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

github source code

0. 들어가며

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

1. 초기 설계 - 문장으로 나열해 보기

1.0 큰 틀

  • 필요한 객체는 게임판, 딜러, 유저, 덱, 게임 규칙
  • 게임판은 카드가 올려져있는 실제 게임 판을 의미 한다.
  • 딜러는 카드를 나눠주고 규칙을 적용하고, 돈을 주거나 받는다.
  • 덱은 카드만 랜덤으로 뽑아 출력 한다.c
  • 게임 규칙은 딜러가 사용할 수 있고, 유저도 사용 가능하다.
  • 딜러 규칙, 사용자 규칙 또한 나눠져 있다.

1.1 딜러

  • 딜러는 유저에게 카드를 준다.
  • 딜러는 카드덱에 유일하게 접근 가능하다.
  • 딜러도 카드덱에서 다음장에 나올 카드를 모른다.

1.2 유저

  • 유저는 딜러에게 카드를 요청한다.
  • 유저는 각 게임의 턴마다 카드를 요청할 수 있다.
  • 유저는 각 게임의 턴마다 서랜더를 요청할 수 있다.

1.3 큰 틀의 그림

블랙잭의실제모델

  • 규칙은 모두에게 적용되지만, 유저와 딜러는 적용되는 규칙이 다르다.
  • 덱은 딜러만 가지고 있으며, 유저는 딜러에게 덱을 요청한다.
  • 블랙잭의 덱은 52장이다.

2. Java로 BlackJack Deck을 구현하기

  • Suit
  • Suit, 스페이드, 다이아몬드, 클로버, 하트 4가지로 고정되어 있는 값.
package personal.cjh.BlackJack.card;

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

  private String Property;

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

  public String getProperty() {
    return Property;
  }
}
  • Denomination
  • 끗수를 뜻함
  • 스페이드의 모든 끗수를 한벌, 한 세트라고 명함
package personal.cjh.BlackJack.card;

/**
 * 끗수 Enum
 * 블랙잭 게임의 끗수, 즉 숫자 1~10, J, Q, K, A를 표시하는 Enumeration
 */
public enum Denomination {
  ACE("1"), TWO("2"), THREE("3"), FOUR("4"),
  FIVE("5"), SIX("6"), SEVEN("7"), EIGHT("8"),
  NINE("9"), TEN("10"), JACK("10"), QUEEN("10"),
  KING("10");

  private String Property;

  Denomination(String property) {
    this.Property = property;
  }
  public String getProperty() {
    return Property;
  }
}
  • Card
  • Suit 한개와 Denomination 끗수 한개를 가지는 한장의 카드
  • interface, abstract, class 등등 모든 상황을 고려해 Card를 작성해 보려 했지만 사용자 즉, 이 코드를 사용하는 코더 입장에서 사용하기 불편한 점이 많아, 속성을 줄임
  • 추상화 측면에서 Card는 Suit 한가지와 Denomination 한가지를 가져야 하지만, 해당 클래스는 없음.
  • 이부분이 리팩토링 포인트가 될듯
package personal.cjh.BlackJack.card;

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

public abstract class Card {
  public abstract ImmutablePair<Suit, Denomination> getCard();
}
  • Deck
  • Suit 4가지는 각각 한세트의 끗수를 가지며 각각의 카드가 모여 52개의 카드를 생성함
  • 52개의 카드 갯수는 BlackJack 게임 기준
package personal.cjh.BlackJack.deck;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.apache.commons.lang3.tuple.ImmutablePair;
import personal.cjh.BlackJack.card.Card;
import personal.cjh.BlackJack.card.Denomination;
import personal.cjh.BlackJack.card.Suit;

public class Deck extends Card {
  private final List<ImmutablePair<Suit, Denomination>> deck = new ArrayList<>();
  private final Random random = new Random();

  public Deck() {
    for (int i = 0; i < Suit.values().length; i++) {
      for (int j = 0; j < Denomination.values().length; j++) {
        ImmutablePair<Suit, Denomination> temp
            = new ImmutablePair<>(Suit.values()[i], Denomination.values()[j]);
        deck.add(temp);
      }
    }
  }

  @Override
  public ImmutablePair<Suit, Denomination> getCard() {
    int index = random.nextInt(deck.size());
    ImmutablePair<Suit, Denomination> card = deck.get(index);
    deck.remove(index);
    return card;
  }

  public List<ImmutablePair<Suit, Denomination>> getDeck() {
    return deck;
  }
}
  • Test
  • 이제 최종 딜러가 사용할 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 org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;
import personal.cjh.BlackJack.card.Denomination;
import personal.cjh.BlackJack.card.Suit;

public class DeckTest {

  @Test
  public void 덱이_싱글톤인지_검사한다() {
    // 첫번째 덱을 생성한다.
    Deck deck = new Deck();

    // 첫번째 덱의 getDeck 함수를 써서 받은 덱이 같은 덱인지 확인
    assertTrue(deck.getDeck() == deck.getDeck());
    assertFalse(deck == null);
    assertTrue(deck instanceof Deck);
    assertTrue(deck.getDeck().size() == deck.getDeck().size());

    // 카드를 뽑는다.
    deck.getCard();

    // 카드를 뽑아도 같은 객체인지 검사한다.
    assertTrue(deck.getDeck() == deck.getDeck());
    assertFalse(deck == null);
    assertTrue(deck instanceof Deck);
    assertTrue(deck.getDeck().size() == deck.getDeck().size());

    // 두번째 덱을 만든다.
    Deck deck2 = new Deck();

    // 두번째 덱의 getDeck 함수를 써서 받은 덱이 같은 덱인지 확인
    assertTrue(deck2.getDeck() == deck2.getDeck());
    assertFalse(deck2 == null);
    assertTrue(deck2 instanceof Deck);
    assertTrue(deck2.getDeck().size() == deck2.getDeck().size());

    // 카드를 뽑는다.
    deck2.getCard();

    // // 카드를 뽑아도 같은 객체인지 검사한다.
    assertTrue(deck2.getDeck() == deck2.getDeck());
    assertFalse(deck2 == null);
    assertTrue(deck2 instanceof Deck);
    assertTrue(deck2.getDeck().size() == deck2.getDeck().size());

    assertFalse(deck.getDeck() == deck2.getDeck());
  }

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

    // 블랙잭 덱은 최초 52장이다.
    assertThat(deck.getDeck().size(), is(52));

    // 카드 뽑기
    Pair<Suit, Denomination> card = deck.getCard();

    // 뽑은 카드가 각 Enum의 객체가 맞는지 검사
    assertThat(card.getLeft(), instanceOf(Suit.class));
    assertThat(card.getRight(), instanceOf(Denomination.class));

    assertThat(deck.getDeck().size(), is(51));

    // 순환을 돌면서 정상적으로 DECK 사이즈가 줄어드는지 검사
    int count = 51;
    while (deck.getDeck().size() != 0) {
      Pair<Suit, Denomination> cd = deck.getCard();
      printCard(cd, 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);
  }
}

테스트 결과

테스트결과

결론

  • 추상화, 인터페이스를 멀리하며 코딩하던 입장으로써, 여러가지 생각을 하며 실제 객체를 코드에 반영하는 일이 어렵다는걸 다시한번 느낌
  • 썩 마음에 드는 코드는 아님
  • Card 클래스는 리팩토링해 조금더 유연하고 추상화 측면에서 만족스런 결과물을 뽑고 싶음
  • enum의 유용함을 느낌
  • abstract, interface를 사용하는 방법은 알지만 왜, 어떨때 적절히 사용해야 하는지 모르는것을 느낌

조금 중구난방에 헤메고 있다……………………..