자바의 Enum은 다양한 일을 할 수 있다. 예전 포스팅에서도 설명했지만 Enum은 당수 상수 모음이 아니라 고차원의 추상 개념을 완벽히 표현할 수 있을 뿐 아니라 각 상수 데이터와 연결 지어 다양한 일을 할 수 있다.
여러분은 Enum을 사용해 if문을 제거할 수 있나요?
예전에 수업에서 코치가 한 말이다. 당시에는 무슨 의미인지 정확히 몰랐지만 미션을 진행하다 아 이렇게 if 분기문을 없앨 수 있구나 깨달은 적이 있었다. 오늘은 우아한테크코스 블랙잭 미션에서 Enum을 어떻게 활용했는지 포스팅하고자 한다.
블랙잭 결과 도출 규칙
딜러와 플레이어가 대결해서 카드패의 숫자 합이 21을 넘지 않으면서 높으면 승리하는 게임
승리 조건
- 블랙잭 : 처음 받은 2장의 카드가 21이면 블랙잭이라고 한다. 블랙잭일 경우 상대가 블랙잭이 아닌 이상 무조건 승리한다.
- 일반 승리 : 블랙잭은 아니지만 21을 넘지 않으면서 상대보다 카드 숫자 합이 크면 승리한다.
- 패배 : 카드패 숫자 합이 21을 넘으면 패배. 이를 버스트라고 한다. 플레이어가 버스트이면 딜러가 버스트라도 무조건 플레이어가 패배한다. 둘 다 버스트가 아니라면 숫자 합이 낮은 사람이 패배한다.
- 무승부 : 둘 다 블랙잭인 경우 || 둘 다 버스트가 아니면서 숫자 합이 같은 경우
미션에서 사용한 도메인 설명
- Card - 트럼프 카드
- Cards - Card의 일급 컬렉션. 딜러나 플레이어가 쥐고 있는 카드 패를 의미한다. isBlackJack(), isBust() 등의 메서드로 카드패가 블랙잭인지 버스트 되었는지 확인할 수 있다.
- BlackJackResult - Cards 2개를 인자로 받아 결과를 도출하는 클래스 (Enum이 아니었다가 Enum으로 바뀔 예정)
블랙잭 게임 결과 도출 기능 Enum 사용 X
public class BlackJackResult {
public static String of(Cards player, Cards dealer) {
if (player.isBlackJack()) {
return "블랙잭!";
} else if ((!player.isBust() && dealer.isBust()) ||
(!player.isBust() && !dealer.isBust() && player.isGreaterThan(dealer))) {
return "이겻다!";
} else if (player.isBust() ||
(!player.isBlackJack() && dealer.isBlackJack()) ||
(!player.isBust() && !dealer.isBust() && dealer.isGreaterThan(player))) {
return "졌다ㅠㅠ";
} else if ((player.isBlackJack()) && dealer.isBlackJack() ||
(!player.isBlackJack() && !dealer.isBlackJack() && player.isSame(dealer))) {
return "비겼군...";
}
return "결과를 찾을 수 없습니다.";
}
}
Enum을 활용하지 않으면 여러 문제들이 생긴다. 일단 if문에 중점을 맞춰보자면 코드가 복잡하고 게임 룰이 변경되면 if문을 고쳐야 하는데 리턴되는 결과와 결과가 되는 기준(조건문)을 연결 짓기 힘들다. 그리고 승리 종류나 패배 종류가 다양해질 경우 if문은 엄청 늘어날 것이다.
그리고 리턴되는 결과가 문자열이다. 게임 결과에 따라 배팅 수익률도 다른데 그런 다른 정보를 표기하기도 힘들고 단순 문자열이기 때문에 오타 때문에 버그가 일어날 수도 있다.
때문에 저 문자열을 Enum 처리하고 싶어질 것이다. 그럼 결과 상수인 Enum과 결과를 도출하는 로직인 이 if문은 따로 둬야할까? 너무 관련이 깊은 녀석들이기 때문에 같이 두면 좋을 것이다. Enum 안에서 if문이 포함된 메서드를 만드는 것도 방법이지만 다른 방법도 있다.
블랙잭 게임 결과 도출 기능 Enum 사용 O
public enum BlackJackResult {
BLACKJACK_WIN( 1.5, (player, dealer) ->
player.isBlackJack() && !dealer.isBlackJack()
),
WIN( 1, (player, dealer) ->
(!player.isBust() && dealer.isBust()) ||
(!player.isBust() && !dealer.isBust() && player.isGreaterThan(dealer))
),
LOSE( -1, (player, dealer) ->
player.isBust() ||
(!player.isBlackJack() && dealer.isBlackJack()) ||
(!player.isBust() && !dealer.isBust() && dealer.isGreaterThan(player))
),
DRAW(0, (player, dealer) ->
(player.isBlackJack()) && dealer.isBlackJack() ||
(!player.isBlackJack() && !dealer.isBlackJack() && player.isSame(dealer))
);
private static final String NOT_EXIST_ERROR = "옯바른 결과를 찾을 수 없습니다.";
private final double profit;
private final BiPredicate<Cards, Cards> predicate;
BlackJackResult(double profit, BiPredicate<Cards, Cards> predicate) {
this.profit = profit;
this.predicate = predicate;
}
public static BlackJackResult of(Cards cards, Cards otherCards) {
return Arrays.stream(values())
.filter(result -> result.predicate.test(cards, otherCards))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(NOT_EXIST_ERROR));
}
// ...
}
Enum에서 BiPredicate를 사용해 각 상수에 람다로 Predicate를 재정의한 모습이다. 이렇게 각 승리, 패배, 무승부 상수에 조건을 람다로 전달하면 of() 메서드에서 Cards 2개를 받아 결과를 리턴해줄 수 있다. if문도 사라지고 수익률 같은 추가적인 정보도 넣어줄 수 있다.
BLACKJACK_WIN, WIN, LOSE, DRAW의 데이터는 본인이 본인 수익률과 Predicate 조건문에 해당하는 것을 알고있다. 이를 데이터들 간의 연관관계를 표현할 수 있다고 말한다. 두 Cards를 받았을 때 본인이 어디에 해당하는지 스스로가 알고 있다. 이를 상태와 행위를 한 곳에서 관리할 수 있다고 표현한다.
BlackJackResult result = BlackJackResult.of(playerCards, dealerCards);
이를 사용하는 클라이언트쪽에서는 Cards 2개를 비교하는 행위와 Cards 2개의 결과(상태)를 한 번에 처리할 수 있다.
참고
'JAVA' 카테고리의 다른 글
상태 패턴 (in 체스 미션) (0) | 2022.04.04 |
---|---|
IllegalArgumentException vs IllegalStateException (0) | 2022.03.31 |
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |
[AssertJ] Iterable and array assertions 활용 (컬렉션 테스트) (0) | 2022.03.03 |
[자바] 객체 내부 컬렉션 데이터 보호하기 (0) | 2022.02.26 |