저번 시간 남은 할 일 체크하면서
위와 같은 말을 남겼는데, 위 방법이 아닌 다른 방법으로 구현했다. (상속을 이용하는 형태)
++ User, Users 라는 이름을 쓸 수 없다 ㅠ(아쉽게도, 이미 그 전 prac들에서 클래스가 정의되어 있어서 조금 어색한 이름을 차용했다. 클래스, 매서드, 변수명은 계속 고민중...)
public enum CardSumStatus {
UNDER, BLACK_JACK, EXCEEDED,
DEALER_MUST_PICK, DEALER_MUST_STAY;
private static final Integer BLACKJACK_COUNT = 21;
private static final Integer DEALER_STANDARD = 16;
public static CardSumStatus getNumberStatusForPlayer(int cardSum){
if (cardSum > BLACKJACK_COUNT) return EXCEEDED;
if (cardSum < BLACKJACK_COUNT) return UNDER;
return BLACK_JACK;
}
public static CardSumStatus getNumberStatusForDealer(int cardSum){
if (getNumberStatusForPlayer(cardSum) == BLACK_JACK) return BLACK_JACK;
if (cardSum <= DEALER_STANDARD) return DEALER_MUST_PICK;
return DEALER_MUST_STAY;
}
}
유저는
1. UNDER - 21 이하
2. BLACK_JACK - 21
3. EXCEEDED - 21 초과
의 상태를 가지며, 딜러는 특수한 조건
("딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.")
이 있기 때문에 다른 해당 조건에 맞춰 주었다.
위 Enum을 User 쪽에선 어떻게 활용해야 할까?
abstract CardSumStatus statusCheck();
위와 같은 매서드를 유저의 상위 추상에 정의했다. 그리고 딜러와 유저는 이 매서드를 오버라이드해서, 자신만의 상태 체크 매서드를 가지면 될 것이다.
그리고 해당 상태 체크 매서드는 게임의 진행 중 카드가 부여되고, 패를 체크하는 시점에 호출되어
"유저" 자신이 어떤 상태를 가지는지 스스로 표현 할 수 있다. 그리고, 이 모든 과정을 지켜보는 Controller 측에서는 유저에게 "표현하라" 고 명령하면 된다.
public class Dealer extends GameUser {
private static final String DEALER_NAME = "Dealer";
public Dealer() { // 딜러는 고정된 이름을 가지도록 의도
super(DEALER_NAME);
}
@Override
public CardSumStatus statusCheck() {
return CardSumStatus.getNumberStatusForDealer(this.getCardSum());
}
}
딜러가 가질 매서드이다. (ForDealer)
GameUser및 하위 클래스 객체는 스스로의 카드 총합을 계산하는 구상 매서드 getCardSum() 을 가진다.
해당 구상 매서드를 이용해서 객체의 상태를 체크하는 로직으로 구성했다.
Enum 클래스에서 static하게 바로 호출하는 것 보다는, Dealer 에게 그 책임을 주는 것이 맞다고 생각해서 위와 같은 형태로 만들었다.
public class Player extends GameUser {
public Player(String name) {
super(name);
}
@Override
public CardSumStatus statusCheck() {
return CardSumStatus.getNumberStatusForPlayer(this.getCardSum());
}
}
익명 클래스?
ex) 조건에 따라 합산하는 부분을, @FunctionalInterface 를 이용해서 해결하는 코드.
@FunctionalInterface
public interface SumWithCondition {
boolean sumCondition(int number);
}
...
class foo {
...
public static void runThread() {
new Thread(() -> System.out.println("Hello from thread")).start();
}
public static int sumAll(List<Integer> numbers) {
return sumByCondition(numbers, number -> true);
}
public static int sumAllEven(List<Integer> numbers) {
return sumByCondition(numbers, number -> number % 2 == 0);
}
public static int sumAllOverThree(List<Integer> numbers) {
return sumByCondition(numbers, number -> number > 3);
}
public static int sumByCondition(List<Integer> numbers, SumWithCondition sumWithCondition){
return numbers.stream().filter(sumWithCondition::sumCondition).reduce(0, Integer::sum);
}
그럼, 이걸 사용하지 않은 이유는?
1.매서드들이 같은 클래스 내에 존재하고, 따라서 그 안에서 자유롭게 호출이 가능하거나,
2.public static 매서드라서 여러 클래스에서 접근 가능한 매서드라는 전제 조건이 필요하다.
(리턴하는 녀석이 Enum, 즉 싱글턴 인스턴스여서 큰 문제가 없다고 해도, 어쨋든 해당 로직이 존재하는 "특정 클래스"에 대한 결합이 높아진다고 생각했다)
public class Players {
private final List<GameUser> users;
public Players(List<GameUser> users) {
this.users = users;
}
public int size() {
return users.size();
}
public void dealCards(Deck cards) {
this.users.forEach(gamer -> gamer.pickCard(cards.popCard()));
}
public List<String> getPlayersNames(){
return this.users.stream().map(GameUser::getName).collect(Collectors.toList());
}
public GameUser findUserByUsername(String username){
return users.stream().filter(user -> user.getName().equals(username)).findAny().orElseThrow(
() -> new IllegalArgumentException("해당하는 유저가 없다.")
);
}
public List<Card> getCardsInHandByUsername(String username){
GameUser user = this.findUserByUsername(username);
return user.getCardsInHand();
}
}
불변, 일급 컬렉션이다.
컨트롤러와 이 컬렉션의 상호작용을 현재는 다음과 같이 구상했다.
컨트롤러 측에서는 Players를 가지고 있으며, 컨트롤러 측에서는 이 녀석들의 이름을 받아서 사용한다. (직접 List 컬렉션을 넘겨주지 않는다.)
컨트롤러측에서는 넘겨 받은 이름을 활용해서, 이름마다 손패가 어떤지 물어본다. 그 후 OutputView 에게 결과를 넘겨준다.
public class BlackjackController {
private final Deck deck;
private final Players players;
public BlackjackController(Deck deck, Players players) {
this.deck = deck;
this.players = players;
}
public void createDeck() {
this.deck.createCardDeck();
this.deck.shuffleDeck();
}
public Card popCard(){
return this.deck.popCard();
}
public int totalPlayerSize(){
return this.players.size();
}
public void dealCards() {
this.players.dealCards(this.deck);
}
}
이제껏 테스트하며 만들어온 그 모든 녀석들을 사용하는 녀석
현재는 Input, OutputView 와의 연결은 없다. (먼저, 테스트 용이한 로직들부터 테스트하기 위함도 있고, 귀찮기도 했다...)
대부분의 로직은 각자 객체들이 알아서 책임지고 실행한다. 컨트롤러는 실행 타이밍을 조율한다.
작은 기능들을 모듈화, 조립해서 큰 녀석으로 만들어가는 과정의 재미를 조금 느껴본 것 같다.
내가 필요로 하는 로직들이 대부분 다른 곳에서 이미 구현되었거나, 이미 구현된 것을 바탕으로 조금만 더 손보면 만들 수 있었다.
특히 테스트가 굉장히 효율적인 수단이구나를 다시 한 번 느꼈다. 내가 직접 느낀 테스트의 장점은
댓글 남겨주시면 더 좋구요~