블랙잭 게임 분석 - 1
이전 글에서 블랙잭 게임을 다시 시작하고자 했다.
이번 프로젝트는 우테코 2기 프리코스에서 진행된 프로젝트이다.
전체 코드를 확인 가능하다.
이 코드는 kouz95 님의 코드를 기반으로 하고 있습니다.
이번엔 패키지 구조와 각 클래스의 구성 하나하나 뜯어보려고 한다.
양이 후덜덜 하겠지.. ㅠ?
요구 사항 분석
패키지 분석
- domain
- card
- Card.class
- CardRepository.class
- Cards.class
- Deck.class
- Symbol.enum
- Type.enum
- controller
- blackjackController.class
- user
- strategy.draw
- DealerDrawStrategy.class
- PlayerDrawStrategy.class
- DrawStrategy.interface
- Dealer.class
- Player.class
- User.interface
- Users.class
- strategy.draw
- BettingMoney.class
- PlayerIntentionType.enum
- PlayerMoneys.class
- ResultType.enum
- ScoreType.enum
- card
- view
- InputView
- OutputView
ㅗㅜㅑ...
이것이야 말로 단일 책임의 끝판왕;;
이제까지 내가한 프로젝트를 바이트 단위로 쪼개버리고 싶다.
나같은 모지리는 클래스 하나하나에 들어있는 숨결까지 온전히 느껴야 한다..
스압 가보즈아.
분석
Card.class
public class Card {
private final Symbol symbol;
private final Type type;
public Card(Symbol symbol, Type type) {
this.symbol = symbol;
this.type = type;
}
public Symbol getSymbol() {
return symbol;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Card card = (Card) o;
return symbol == card.symbol && type == card.type;
}
@Override
public int hashCode() {
return Objects.hash(symbol, type);
}
@Override
public String toString() {
return symbol.getName() + type.getName();
}
}
Enum으로 선언된 Symbol과 Type으로 이루어진 블랙잭 게임의 핵심 객체 카드이다.
별다르게 볼것은 없지만 equlas()와 hashCode가 오버라이딩 되어있는 것이 인상적이다. oop를 이렇게 극한까지 따르시다니
Symbol.enum
public enum Symbol {
ACE(1, "A", score -> score <= 11, () -> 10),
TWO(2, "2"),
THREE(3, "3"),
FOUR(4, "4"),
FIVE(5, "5"),
SIX(6, "6"),
SEVEN(7, "7"),
EIGHT(8, "8"),
NINE(9, "9"),
TEN(10, "10"),
JACK(10, "J"),
QUEEN(10, "Q"),
KING(10, "K");
private final int score;
private final String name;
private final Predicate<Integer> promotionJudge;
private final Supplier<Integer> bonusScore;
Symbol(int score, String name) {
this(score, name, i -> false, () -> 0);
}
Symbol(int score, String name, Predicate<Integer> promotionJudge, Supplier<Integer> bonusScore) {
this.score = score;
this.name = name;
this.promotionJudge = promotionJudge;
this.bonusScore = bonusScore;
}
public int getPoint() {
return score;
}
public String getName() {
return name;
}
public boolean isPromotable(int score) {
return promotionJudge.test(score);
}
public int getBonusPoint() {
return bonusScore.get();
}
}
일단 눈여겨 봐야 할것은 Predicate와 Supplier의 사용이지 않을까 싶다.
앞의 최근 게시물은 모두 여기를 위한 빌드업..!
일단 생성자를 보자
생성자는 두가지가 있다
Symbol(int score, String name)
Symbol(int score, String name, Predicate<Integer> promotionJudge, Supplier<Integer> bonusScore)
이넘의 값을 보면 ACE의 경우 Predicate와 Supplier가 있고 나머지 속성에는 없다.
ACE를 먼저 분석해보자
ACE(1, "A", score -> score <= 11, () -> 10)
두번째 속성인 Predicate는 매개변수를 일정 조건을 통해 검사하여 논리 값을 반환한다.
Integer의 매개변수인 score를 검사해 score <= 11
의 결과를 반환할 것이다.
세번째 속성인 Supplier는 요청 시 함수의 특정 값을 반환하는 제공자이다.
Supplier.get()이 호출되면 () -> 10
을 통해 10을 반환할 것이다.
그렇다면 이 두 속성이 없는 값들의 경우
Symbol(int score, String name) {
this(score, name, i -> false, () -> 0);
}
이 생성자를 타게 되고,
Predicate의 경우 매개 변수 i가 들어오면 상수 false를 반환하는 람다식을 인스턴스로 갖게된다.
Supplier의 경우도 마찬가지로 호출 시 0을 반환하는 인스턴스를 갖는 Enum값이 된다.
CardRepository.class
public class CardRepository {
private static final List<Card> cards;
static {
cards = Arrays.stream(Symbol.values())
.flatMap(CardRepository::mapToCardByType)
.collect(Collectors.toList());
}
private static Stream<Card> mapToCardByType(Symbol symbol) {
return Arrays.stream(Type.values())
.map(type -> new Card(symbol, type));
}
public static List<Card> toList() {
return Collections.unmodifiableList(cards);
}
}
일단 Repository라 함은..
모델에서 데이터베이스에 접근하는 메소드를 사용하기 위한 인터페이스인데..
이 자바 프로젝트에서는 Repository가 어떻게 쓰였는지 살펴봐야 역할을 제대로 알 수 있을것 같다.
일단 클래스 초기화 블럭에서 Card의 일급 컬렉션을 정의해주고 있다.
이전 블랙잭 게임에서 고민했던 enum의 2중 포문 문장을 메서드로 분리해서 깔끔하게 1 depth로 처리한 모습이 인상적이다.
메서드를 살펴보면 바깥쪽 루프는 flatMap이고 안쪽 루프는 map을 사용했다.
- map : 단일 스트림 안의 요소를 원하는 특정 형태로 변환할 수 있다.
- flatMap : 모든 원소를 단일 원소 스트림으로 반환할 수 있다.
map() 및 flatMap() 메소드와 둘 다 중간 스트림 조작이며 다른 스트림을 메소드 출력으로 리턴한다.
map()과 flatMap() 의 주요 차이점은 두 메소드의 리턴 유형이다.
map()객체 스트림이있을 때 연산 을 사용할 수 있으며 스트림의 각 요소에 대해 고유 한 값을 가져와야한다.
일대일 입출력 요소 사이의 매핑.
예를 들어, 직원 스트림에서 모든 직원의 생년월일 을 찾는 프로그램을 작성할 수 있다 .
flatMap()의 경우, 각 입력 요소 / 스트림에 대해 일대 다 매핑이 생성되어 먼저 여러 값을 얻은 다음 모든 입력 스트림의 값을 단일 출력 스트림으로 병합한다.
예를 들어, 텍스트 파일의 모든 줄에서 모든 지구 단어 를 찾기위한 프로그램을 작성할 수 있다.
Java Stream map(), flatMap() 차이
일단 여기까지 봤을때,
Repository의 역할은 카드 객체들을 일급 컬렉션으로 래핑하고 생성해 실제적인 데이터로 만드는 과정을 담당하는 것 같다.
Cards.class
public class Cards {
private static final int INITIAL_CARDS_SIZE = 2;
private final List<Card> cards;
public Cards(List<Card> cards) {
this.cards = cards;
}
public void add(Card card) {
cards.add(card);
}
public boolean isInitialSize() {
return cards.size() == INITIAL_CARDS_SIZE;
}
public boolean isNotInitialSize() {
return cards.size() != INITIAL_CARDS_SIZE;
}
public int getPoint() {
int point = cards.stream()
.map(Card::getSymbol)
.mapToInt(Symbol::getPoint)
.sum();
int bonusPoint = cards.stream()
.map(Card::getSymbol)
.filter(symbol -> symbol.isPromotable(point))
.mapToInt(Symbol::getBonusPoint)
.findFirst()
.orElse(0);
int resultPoint = point + bonusPoint;
return ScoreType.of(resultPoint).getScore(resultPoint);
}
public List<Card> toList() {
return Collections.unmodifiableList(cards);
}
}
이 객체를 Card의 일급 컬렉션으로 오해할 수도 있지만 이 객체의 역할은 딜러와 플레이어들의 핸드를 담당하는 작은 CardList이다.
핸드의 점수 계산과 장 수 카운팅을 담당하는데 인상적인 부분은
isInitialSize(), isNotInitialSize()
메서드 였다.
한가지 메서드를 그냥 ! 부정 해버리면 되는데 가독성을 위해 따로 뺀거 같은데,
이게 좋은지 생각 해봐야겠다.
Deck.class
public class Deck {
private final Stack<Card> deck;
private Deck(Stack<Card> deck) {
this.deck = deck;
}
public static Deck of(List<Card> cards) {
Stack<Card> deck = new Stack<>();
cards.forEach(deck::push);
Collections.shuffle(deck);
return new Deck(deck);
}
public Card pop() {
return deck.pop();
}
}
게임을 진행하게 될 카드의 덱이다.
일급 컬렉션인 Cards에서 연산을 위해 Stack으로 바꾸어 준 것이다.
간단하지만 깔끔한 코드가 인상적이었고, 프로젝트 종종 of() 메서드가 나오는데 어떤 의미로 of라는 네이밍을 한건지 궁금하다.
구성하다 이런 의미인가?..
User.interface
public abstract class User {
private static final int BLACKJACK_SCORE = 21;
private static final int INITIAL_HANDS_SIZE = 2;
protected Cards hands;
protected DrawStrategy drawStrategy;
protected User() {
this.hands = new Cards(new ArrayList<>());
}
public void proceedInitialPhase(Deck deck) {
for (int i = 0; i < INITIAL_HANDS_SIZE; i++) {
hands.add(deck.pop());
}
}
public boolean canDrawMore() {
return drawStrategy.canDraw(hands.getPoint());
}
public void receive(Card card) {
hands.add(card);
}
public boolean isNotBlackJack() {
return hands.isNotInitialSize() || hands.getPoint() != BLACKJACK_SCORE;
}
public int getScoreMinusBy(User compared) {
return hands.getPoint() - compared.hands.getPoint();
}
public Cards openAllCards() {
return hands;
}
public abstract Cards openInitialCards();
@Override
public abstract String toString();
}
User의 근간을 이루는 인터페이스.
어떤 메서드가 있는지 가볍게 보고 넘어가면 될것이다.
인상적인 것은 protected 접근 제어자 인데, 난 실제로 처음 접한다.
- protected : 같은 패키지 내에서, 다른 패키지의 자손 클래스에서 접근 가능
Dealer.class
public class Dealer extends User{
private static final int FIRST_CARD_INDEX = 0;
public Dealer() {
drawStrategy = new DealerDrawStrategy();
}
@Override
public Cards openInitialCards() {
return new Cards(Collections.singletonList(hands.toList().get(FIRST_CARD_INDEX)));
}
@Override
public String toString() {
return "딜러";
}
}
User를 상속받는 Dealer이다.
굉장히 간결한데 Dealer가 가지고 있는 책임이라곤, 요구사항의 한장의 카드를 공개하는 것과 toString 뿐이다.
Player.class
public class Player extends User{
private final String name;
public Player(String name) {
if (name.isEmpty()) {
throw new IllegalArgumentException("이름이 빈 문자열입니다.");
}
this.name = name;
drawStrategy = new PlayerDrawStrategy();
}
public Player(String name, List<Card> cards) {
this(name);
hands = new Cards(cards);
}
@Override
public Cards openInitialCards() {
return hands;
}
@Override
public String toString() {
return name;
}
}
Dealer와 큰 차이는 없다.
Users.class
public class Users implements Iterable<User> {
private static final int MAX_PLAYERS_COUNT = 8;
private static final int MIN_PLAYERS_COUNT = 2;
private static final String SPLIT_DELIMITER = ",";
private final List<User> users;
private Users(List<User> players, User dealer) {
players.add(dealer);
if (players.size() > MAX_PLAYERS_COUNT) {
throw new IllegalArgumentException("블랙잭의 최대 인원은 8명입니다.");
}
if (players.size() < MIN_PLAYERS_COUNT) {
throw new IllegalArgumentException("블랙잭의 최소 인원은 2명입니다.");
}
this.users = players;
}
public static Users of(List<User> players, User dealer) {
return new Users(players, dealer);
}
public static Users of(String playerNames, User dealer) {
return of(Arrays.stream(playerNames.split(SPLIT_DELIMITER))
.map(String::trim)
.map(Player::new)
.collect(Collectors.toList()), dealer);
}
public List<Player> getPlayers() {
return users.stream()
.filter(user -> user.getClass() == Player.class)
.map(user -> (Player) user)
.collect(Collectors.toList());
}
@Override
public Iterator<User> iterator() {
return users.iterator();
}
}
User를 상속 받은 객체들의 관리를 위한 일급 컬렉션 객체이다.
특이한 점은Iterable<User>
를 구현하고 있다는 것
Iterable 인터페이스는 최상위 인터페이스로 Collection 인터페이스에서 상속하고 있다.
내부를 살펴보면 디폴트 메소드인 forEach 메소드와 spliterator 메소드를 가지고 있고 추상 메소드로 iterator 메소드를 가지고 있다.
Spliterator 인터페이스는 기존 존재했던 반복자인 Iterator 와 비슷하지만
자바 8에서 추가된 병렬 작업에 특화된 인터페이스이다.
Iterable를 통해 forEach문을 사용한다는 것은 stream의 생성비용 없이 반복문을 사용할 수 있다는 것이다.
Iterable 을 구현한 클래스에서는
반드시 iterator() 메소드를 override해야 한다.
Collection인터페이스에서는 Iterable 을 상속하고 있기 때문에 Collection 인터페이스를 상속하는 인터페이스에서도 Iterable을 상속하고 있고 List, Queue, Set 인터페이스 등을 구현하는 ArrayList, LinkedList 클래스 등에서는 Iterable의 Iterator 메소드를 override하고 있다.
때문에 우리가 Collection을 쓰면서 for-each-loop를 사용할 수 있는 것이다.
Iterable을 구현하지도 않은 객체에서 iterator() 메소드를 사용해서 반복하는 for-each loop를 어떻게 사용할 수 있을까?
결론은 for-each loop를 일반 for문으로 컴파일러가 번역해준다.
Iterator 인터페이스와 Iterable 인터페이스
Author And Source
이 문제에 관하여(블랙잭 게임 분석 - 1), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@jaca/블랙잭-게임-분석저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)