객체 지향 설계 연습하기 - 블랙잭 (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;
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;
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();
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();
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();
assertThat(deck.getDeck().size(), is(52));
Pair<Suit, Denomination> card = deck.getCard();
assertThat(card.getLeft(), instanceOf(Suit.class));
assertThat(card.getRight(), instanceOf(Denomination.class));
assertThat(deck.getDeck().size(), is(51));
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를 사용하는 방법은 알지만 왜, 어떨때 적절히 사용해야 하는지 모르는것을 느낌
조금 중구난방에 헤메고 있다……………………..