깃 저장소
학습 전
학습 후
어려웠던 부분
- 클래스 책임을 제대로 처리하지 못하여, 로직이 분산되어 코드를 읽기 힘들었음
- 테스트시 딜러와 플레이어가 가진 패를 초기화하는 방법을 생각지 못해 헤매었슴
- 블랙잭 게임에 대한 이해 부족 (=요구사항 이해 부족)
특히나 블랙잭 게임의 승패 판정할 때 딜러와 플레이어간의 1:1 비교를 이해하지 못해 삽질을 몇시간 했었다.
(전체 중에서 한명만 승리하는 걸로 착각)
*블랙잭 게임 ?
- 플레잉 카드를 가지고 21에 가까이 만들면 이기는 게임 (최대 21)
- 조커를 제외한 52장의 카드 덱을 사용한다
- 카드 숫자 계산에서 K/Q/J는 각 10점으로 계산한다. 예외로 ACE는 1 또는 11로 계산할 수 있다.
Insurance 와 같은 추가 규칙은 구현에서 제외
*용어
+ DEAL : 게임 시작시 처음 패를 2장 나누는 행위
+ Blackjack, Hit, Stay, Bust
① Blackjack: 처음 패를 2장 받았을 때 카드 합이 21인 상태 (예로 (♥ K, ♥ ACE) / (♣J , ♦ACE) )
② Hit : 카드 합이 21을 초과하지 않은 상태
③ Stay : 카드 합이 21을 초과하지 않으면서 턴을 종료한 상태
④ Bust : 카드 합이 21을 초과하여 턴이 종료된 상태
*최종 판정 (경우의 수)
- Player가 Blackjack으로 승리하는 경우 배율을 1.5배로 한다
- 무승부인 경우 배팅한 금액을 돌려 받는다
- Dealer가 Bust인 경우 살아있는 Player는 점수와 상관없이 승리한다 (기본 배율 1.0배)
- [x] 딜러만 블랙잭인 경우
- [x] 플레이어만 버스트인 경우
- [x] 둘 다 버스트가 아니고 딜러의 Score가 더 높은 경우
Player 승리하는 경우
- [x] 플레이어만 블랙잭인 경우
- [x] 딜러만 버스트인 경우
- [x] 둘 다 버스트가 아니고 플레이어의 Score가 더 높은 경우
무승부인 경우
- [x] 둘 다 블랙잭인 경우
- [x] 둘 다 Score가 같은 경우
복습 - 블랙잭 게임
① 플레이어 이름을 입력한다(이때 콤마(,)로 구분)
② 플레이어 별 배팅 금액을 입력한다
③ 딜러와 플레이어는 각각 초기 카드 2장씩 받는다
④ 딜러와 플레이어의 카드를 공개한다. 이때 딜러는 카드 한장만 공개하고, 플레이어는 모두 공개한다
플레이어 턴
④ 드로우 여부를 사용자 입력 y/n으로 결정한다
- y : 한 장 드로우 한다
- n : 턴을 넘긴다
딜러 턴
⑤ 딜러의 카드 총합이 16이하이면 한장 드로우하고, 17이상이면 드로우 하지 않는다
턴이 모두 종료된 후
⑥ 딜러, 플레이어가 보유한 각 카드와 합계를 출력한다 (카드는 콤마로 연결)
⑦ 딜러, 플레이어의 최종 수익을 출력한다
[결과 화면]
1. 카드, 카드 덱 구현
enum Pattern 생성
public enum Pattern {
DIAMOND("♦"),
CLOVER("♣"),
SPADE("♠"),
HEART("♥");
// 생략
}
enum Denomination 생성 (*denomination : 끗수)
public enum Denomination {
ACE(1, "A"),
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"),
KING(10, "K"),
QUEEN(10, "Q"),
JACK(10, "J");
// 생략
}
Deck 클래스
- 생성자 호출 시 52장의 카드를 신규 생성
- draw() 할 경우
① 카드 한 장을 뽑아 반환한다
② 뽑은 카드를 덱에서 제거한다
이때 ArrayList를 사용할 경우 remove()시 copy와 shift 연산이 일어나 좋지 않다고 생각했다
그래서 데이터 추가/삭제시 성능이 좋은 LinkedList를 사용했다.
public class Deck {
private List<Card> deck;
public Deck() {
this.deck = create();
}
public static List<Card> create() {
List<Card> playingCard = Arrays.stream(Pattern.values())
.flatMap(pattern -> Arrays.stream(Denomination.values()).map(denomination -> new Card(pattern, denomination)))
.collect(Collectors.toCollection(LinkedList::new));
Collections.shuffle(playingCard); // 카드를 섞는다
return playingCard;
}
}
"크.. Stream API도 사용하고 깔끔하게 했다" 라고 처음에 생각했다
시간이 지나서 다시보니,
카드 생성하는데 Stream API 사용하는 것보다 2중 for문이 성능면이나 가독성면에서 좋을거라는 생각이 들었다.
그리고 List 자료구조를 사용했는데,
Stack(LIFO)을 사용해서 pop()을 호출하는게 카드 드로우 느낌을 살리는 거 같아 변경하였다
public class Deck {
// 필드, 생성자
public static Deque<Card> create() {
Deque<Card> result = new ArrayDeque<>(); // Stack보다 성능 이점 존재
Pattern[] patterns = Pattern.values();
Denomination[] denominations = Denomination.values();
for(Pattern pattern : patterns) {
for(Denomination denomination : denominations) {
result.push(new Card(pattern, denomination));
}
}
Collections.shuffle(result);
return result;
}
}
테스트 코드
class DeckTest {
@Test
void 덱_사이즈는_52이다() {
Deck deck = new Deck();
assertThat(deck.size()).isEqualTo(Deck.INIT_SIZE);
}
@Test
void 카드_드로우했을때_사이즈는_한장이_줄어든다() {
Deck deck = new Deck();
deck.draw();
assertThat(deck.size()).isEqualTo(deck.INIT_SIZE - 1);
}
}
2. Cards 구현 (일급 컬렉션)
- Player와 Dealer는 각각 손에 카드를 보유한다 -- 속성
- 그리고 카드패는 아래의 공통된 행동을 한다 -- 행위
① 드로우한 카드를 패에 추가한다
② 총 점수를 구한다
③ 카드 패를 보여준다, 이때 콤마(,)로 연결된다
④ 블랙잭인지 판단한다 (boolean)
⑤ 버스트인지 판단한다 (boolean)
중복으로 Player와 Dealer에 각각 구현하기 보다는
List<Card>를 일급 컬렉션으로 만들어서 관리하는게 좋겠다는 생각이 들었다
*일급 컬렉션 ?
- Collection 자료를 원시값으로 보고 Wrapping한 클래스로, 다른 멤버 변수가 없는 상태를 가진다
- 아래 효과를 얻을 수 있다
① 컬렉션의 불변성 보장 (사이드 이펙트 최소화)
② 관련 비즈니스 함수를 한 곳에서 관리 가능 (유지보수 용이, SRP 원칙 준수)
③ 해당 컬렉션에 대한 동일한 입력과 출력을 반환 (안전성 보장)
Cards 클래스 생성
public class Cards {
private static final int TWO = 2;
private static final int NAX_POINT = 21;
private List<Card> cardList = new ArrayList<>();
public Cards() {
}
// 메소드
}
① 드로우한 카드를 패에 추가한다
public void add(Card card) {
this.cardList.add(card);
}
② 총 점수를 구한다
- 합계 구한 후, ACE 보유 개수만큼 반복문을 돌며 21을 초과하지 않을 경우 10을 추가할 수 있도록 하였다
- Score 클래스는 점수를 담당하는 Wrapper Class 이다 -- 이 부분은 조금 컨닝을 하였다 😉
- Score 클래스가 추가되기는 하지만, ACE일때 10을 더할지 말지의 여부를 Score 클래스로 감추게 되니 코드가 간결해진 느낌을 받았다
public Score score() {
int sum = sum();
int aceCount = aceCount();
Score score = new Score(sum);
for(int i = 0; i < aceCount; i++) {
score = score.plusTenIfNotBust();
}
return score;
}
private int sum() {
int result = 0;
for(Card card : cardList) {
result += card.getPoint();
}
return result;
}
private int aceCount() {
int result = 0;
for(Card card : cardList) {
if(card.isAce()) {
result += 1;
}
}
return result;
}
그리고 반복문 부분은 Stream API를 적용하였다
private int sum() {
return this.cardList.stream()
.map(Card::getPoint)
.reduce(0, Integer::sum);
}
private int aceCount() {
return (int) this.cardList.stream()
.map(Card::isAce)
.count();
}
③ 카드 패를 보여준다, 이때 콤마(,)로 연결된다
public String joinCardList() {
return this.cardList.stream()
.map(Card::show)
.collect(joining(", "));
}
④ 블랙잭인지 판단한다 (boolean)
public boolean isBlackjack() {
return cardList.size() == TWO && score().isSame(NAX_POINT);
}
⑤ 버스트인지 판단한다 (boolean)
public boolean isBust() {
return score().over(NAX_POINT);
}
Score 클래스를 만드는게 클래스 볼륨만 추가되는게 아닌가 싶었는데
isBlackjack(), isBust() 메소드와 같이 Score 객체에게 메시지를 던진다는 점에서 객체 지향적인 느낌을 받아 좋았다
단순히 원시값을 포장했을 뿐인데.. 이런 효과가!!
3. Dealer, Player 구현 (상속과 인터페이스)
Player 속성
- 이름 : (사용자 입력)
- 배팅 금액 : 초기 0
- 카드 패 : 초기 패 없음(new Cards())
Dealer 속성
- 이름 : "딜러" 고정
- 배팅 금액 : 초기 0
- 카드 패 : 초기 패 없음(new Cards())
그리고 공통 행위를 정의하여 아래와 같이 클래스를 생성하였다
*drawable()은 딜러의 카드 점수가 16이하인지 확인하는 메소드이다
*showFirstCard()는 처음 패 공개시 딜러는 카드 패 한장만 공개해야 하므로 추가하였다
테스트 코드
class ParticipantTest {
@Test
void isBlackjack() {
Participant player = new Player("플레이어1");
player.draw(new Card(Pattern.SPADE, Denomination.ACE));
player.draw(new Card(Pattern.HEART, Denomination.KING));
assertThat(player.isBlackjack()).isTrue();
}
@Test
void isBust() {
Participant player = new Player("플레이어1");
player.draw(new Card(Pattern.SPADE, Denomination.KING));
player.draw(new Card(Pattern.HEART, Denomination.QUEEN));
player.draw(new Card(Pattern.HEART, Denomination.TWO));
assertThat(player.isBust()).isTrue();
}
@Test
void score() {
Participant player = new Player("플레이어1");
player.draw(new Card(Pattern.SPADE, Denomination.KING));
player.draw(new Card(Pattern.HEART, Denomination.QUEEN));
Score score = player.score();
assertThat(score).isEqualTo(new Score(20));
}
}
4. Rule 클래스 구현 (최종 판정)
Rule 클래스에게 딜러와 플레이어 정보를 받아 최종적인 결과값을 구하도록 하는 책임을 부여했다
* 최종 화면 출력
각 손패와 점수를, 그리고 최종 수익을 구하여 OutputView에 전달하도록 하였다
*최종 판정 (경우의 수)
- Player가 Blackjack으로 승리하는 경우 배율을 1.5배로 한다
- 무승부인 경우 배팅한 금액을 돌려 받는다
- Dealer가 Bust인 경우 살아있는 Player는 점수와 상관없이 승리한다 (기본 배율 1.0배)
- [x] 딜러만 블랙잭인 경우
- [x] 플레이어만 버스트인 경우
- [x] 둘 다 버스트가 아니고 딜러의 Score가 더 높은 경우
Player 승리하는 경우
- [x] 플레이어만 블랙잭인 경우
- [x] 딜러만 버스트인 경우
- [x] 둘 다 버스트가 아니고 플레이어의 Score가 더 높은 경우
무승부인 경우
- [x] 둘 다 블랙잭인 경우
- [x] 둘 다 Score가 같은 경우
테스트 코드 작성
class RuleTest {
@Test
void dealerWin_딜러만_블랙잭() {
Dealer dealer = new Dealer();
dealer.draw(new Card(HEART, ACE)); // blackjack
dealer.draw(new Card(CLOVER, QUEEN));
Player player = new Player("플레이어");
player.draw(new Card(CLOVER, ACE)); // 20
player.draw(new Card(HEART, NINE));
player.initBetAmount(1000);
Rule rule = new Rule(dealer, Arrays.asList(player));
Map<String, Double> resultMap = rule.getResultMap();
assertThat(resultMap)
.containsEntry(Dealer.DEFAULT_NAME, 1000.0)
.containsEntry("플레이어", -1000.0);
}
// 반복..
}
Rule 클래스 생성
- 최종 수익을 Map<String, Double> 자료 구조로 반환하는 getResultMap() 구현
- 순서를 보장하기 위해 LinkedHashMap() 사용
public class Rule {
private Dealer dealer;
private List<Participant> players;
public Rule(Dealer dealer, List<Participant> players) {
this.dealer = dealer;
this.players = players;
}
public Map<String, Double> getResultMap() {
Map<String, Double> resultMap = new LinkedHashMap<>();
for(Participant player : this.players) {
double profit = judgement(this.dealer, player);
resultMap.put(this.dealer.getName(), resultMap.getOrDefault(this.dealer.getName(), 0.0) - (profit));
resultMap.put(player.getName(), profit);
}
return resultMap;
}
private double judgement(Dealer dealer, Participant player) {
if(isDealerWin(dealer, player)) { // 플레이어가 진 경우
return -(player.getBetAmount());
}
if(isPlayerWin(dealer, player)) {
return calculateProfit(player);
}
return 0.0;
}
private double calculateProfit(Participant player) {
int betAmount = player.getBetAmount();
if(player.isBlackjack()) {
return betAmount * 1.5; // TODO. 매직넘버
}
return betAmount;
}
private boolean isDealerWin(Dealer dealer, Participant player) {
return (dealer.isBlackjack() && !player.isBlackjack())
|| (!dealer.isBust() && player.isBust())
|| (!dealer.isBust() && !player.isBust() && dealer.score().greaterThan(player.score()));
}
private boolean isPlayerWin(Dealer dealer, Participant player) {
return (!dealer.isBlackjack() && player.isBlackjack())
|| (dealer.isBust() && !player.isBust())
|| (!dealer.isBust() && !player.isBust() && player.score().greaterThan(dealer.score()));
}
}
Score 클래스 만들어 둔 게 여기서 빛을 내었다
"객체와 객체를 비교하는 것이 객체 지향 사고이다" 라는 말처럼
지금까지 이런 코드를 작성한 적 없던 내가 조금씩 변해가고 있다는게 뭉클했다 🥺
#isDealerWin()
player.score().greaterThan(dealer.score())
#isPlayerWin()
player.score().greaterThan(dealer.score())
그리고 테스트 코드 실행시 정상적으로 동작하는 것을 확인했다
# 리팩토링 - 매직 넘버
- 플레이어가 블랙잭으로 승리한 경우 배팅금액에 1.5배를 지급해야 하는데, 그냥 조건문에 파라미터를 생으로 기재를 해두었다
- 매직 넘버를 사용하게 되면 의미 불명이라 이해가 어렵고, 수정시 변경 범위를 좁히기 힘들어 누락할 수 있는 단점이 있다
- 이를 개선하기 위해 클래스 정적 변수로 상수 선언하여 사용하도록 하였다
변경 전)
private double calculateProfit(Participant player) {
int betAmount = player.getBetAmount();
if(player.isBlackjack()) {
return betAmount * 1.5; // TODO. 매직넘버
}
return betAmount;
}
변경 후)
private static final double BLACKJACK_RATE = 1.5;
private double calculateProfit(Participant player) {
int betAmount = player.getBetAmount();
if(player.isBlackjack()) {
return betAmount * BLACKJACK_RATE;
}
return betAmount;
}
# 리팩토링 - Rule 테스트 코드
Rule 테스트시 초기 데이터 설정하는 부분이 반복되어 피로도가 높았다
변경 전)
class RuleTest {
@Test
void dealerWin_딜러만_블랙잭() {
Dealer dealer = new Dealer();
dealer.draw(new Card(HEART, ACE)); // blackjack
dealer.draw(new Card(CLOVER, QUEEN));
Player player = new Player("플레이어");
player.draw(new Card(CLOVER, ACE)); // 20
player.draw(new Card(HEART, NINE));
player.initBetAmount(1000);
Rule rule = new Rule(dealer, Arrays.asList(player));
Map<String, Double> resultMap = rule.getResultMap();
assertThat(resultMap)
.containsEntry(Dealer.DEFAULT_NAME, 1000.0)
.containsEntry("플레이어", -1000.0);
}
//..
}
- 케이스마다 draw(..)로 데이터 설정했는데, 한번에 설정할 수 있는 방법이 없는지 고민을 했다
- 처음 setter를 사용하는 방법을 생각했지만, setter 개방할 경우 의도치 않은 곳에서 변경이 일어나면 문제 발생할 수 있기 때문에 이는 좋은 방식은 아닌거 같았다 ( 다시 생각해보니 웹 개발에서나 그렇지, 구현 과제에서는 setter도 괜찮은게 아닌가 싶다 )
+ 테스트를 위한 데이터 초기화시 생성자를 이용하는게 좋다는 "자바지기"님의 말씀이 떠올라 생성자 추가하여 테스트 데이터 초기화 할 수 있도록 변경하였다
+ 추가로
① Fixture 클래스를 만들어 테스트에 사용할 Cards 데이터를 준비하고, 재활용함
② @ParameterizedTest + @MethodSource로 반복되는 테스트 줄임
변경 후)
class RuleTest {
public static Stream<Arguments> dealerWinDataProvider() {
return Stream.of(
Arguments.arguments("딜러만 블랙잭인 경우", ACE_QUEEN_BLACKJACK, ACE_NINE_20),
Arguments.arguments("플레이어가 버스트인 경우", ACE_NINE_20, QUEEN_NINE_FIVE_25_BUST),
Arguments.arguments("둘 다 버스트 아니고, 딜러 점수가 높은 경우", TEN_NINE_19, NINE_EIGHT_17)
);
}
@ParameterizedTest(name = "케이스 : {0}")
@MethodSource("dealerWinDataProvider")
void dealerWin(String title, Cards dealerCards, Cards playerCards) {
Dealer dealer = new Dealer(dealerCards);
Player player = new Player("플레이어", playerCards);
player.initBetAmount(1000);
Rule rule = new Rule(dealer, Arrays.asList(player));
Map<String, Double> resultMap = rule.getResultMap();
assertThat(resultMap)
.containsEntry(Dealer.DEFAULT_NAME, 1000.0)
.containsEntry("플레이어", -1000.0);
}
public static Stream<Arguments> playerWinDataProvider() {
return Stream.of(
Arguments.arguments("플레이어만 블랙잭인 경우", ACE_NINE_20, ACE_QUEEN_BLACKJACK, -1500.0, 1500.0),
Arguments.arguments("딜러가 버스트인 경우", QUEEN_NINE_FIVE_25_BUST, TEN_NINE_19, -1000.0, 1000.0),
Arguments.arguments("둘 다 버스트 아니고, 플레이어 점수가 더 높은 경우",NINE_EIGHT_17, TEN_NINE_19, -1000.0, 1000.0)
);
}
@ParameterizedTest(name = "케이스 : {0}")
@MethodSource("playerWinDataProvider")
void playerWin(String title, Cards dealerCards, Cards playerCards, double dealerProfit, double playerProfit) {
Dealer dealer = new Dealer(dealerCards);
Player player = new Player("플레이어", playerCards);
player.initBetAmount(1000);
Rule rule = new Rule(dealer, Arrays.asList(player));
Map<String, Double> resultMap = rule.getResultMap();
assertThat(resultMap)
.containsEntry(Dealer.DEFAULT_NAME, dealerProfit)
.containsEntry("플레이어", playerProfit);
}
public static Stream<Arguments> drawDataProvider() {
return Stream.of(
Arguments.arguments("둘 다 블랙잭인 경우", ACE_KING_BLACKJACK, ACE_QUEEN_BLACKJACK),
Arguments.arguments("점수가 같은 경우", NINE_KING_19, TEN_NINE_19)
);
}
@ParameterizedTest(name = "케이스 : {0}")
@MethodSource("drawDataProvider")
void draw(String title, Cards dealerCards, Cards playerCards) {
Dealer dealer = new Dealer(dealerCards);
Player player = new Player("플레이어", playerCards);
player.initBetAmount(1000);
Rule rule = new Rule(dealer, Arrays.asList(player));
Map<String, Double> resultMap = rule.getResultMap();
assertThat(resultMap)
.containsEntry(Dealer.DEFAULT_NAME, 0.0)
.containsEntry("플레이어", 0.0);
}
}
5. 상태 패턴, 정적 팩토리 메소드 적용하기
상태 패턴(State)
객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있는 디자인 패턴
마치 객체의 클래스가 바뀌는 것과 같은 효과를 얻을 수 있다
= 객체 내부 상태 변경에 따라 객체의 행동이 달라지는/확장되는 패턴
장점
- 상태에 따른 동작을 개별 클래스로 옮겨서 관리할 수 있다
- Context 기존 코드를 변경하지 않고 새로운 State 추가할 수 있다 (OCP 원칙 준수) **
- 코드 복잡도를 줄일 수 있다 (if 조건문 같은 부분)
단점
- 클래스 복잡도가 증가한다 → 오버엔지니어링이 될 수 있지만 가독성이 좋다면 ok라 생각
처음에는 너무 어려워서 헤드퍼스트 책이나 기술 블로그를 읽고, 하나하나 구현 하였다
우선 4가지 상태(Hit, Blackjack, Bust, Stay)와 행동에 대해 그림을 그려보았다
- State 구현체는 Cards, 배율 속성 가진다 (Hit : 배율 없음 / Bust : -1.0 / Blackjack : 1.5 / Stay : 1.0)
- 처음 Deal 경우 State는 Hit 또는 Blackjack 상태를 가진다
- Hit 상태에서 draw(카드) 하는 경우
① 카드 합계가 21이하이면 Hit를 반환한다
② 카드 합계가 21초과이면 Bust를 반환한다
- Blackjack / Bust / Stay 상태에서 draw(카드) 하는 경우 UnsupportedOperationException을 던진다
- 각 플레이어와 딜러의 턴 종료시 상태가 Hit인 경우 Stay로 상태 변경한다
State 인터페이스
public interface State {
State draw(Card card);
boolean isFinished();
Cards cards();
double profit(double betAmount);
State stay();
}
StateFactory 정적 메소드 생성
public class StateFactory {
public static State create(Card one, Card two) {
Cards cards = new Cards(one, two);
if(cards.isBlackjack()) {
return new Blackjack(cards);
}
return new Hit(cards);
}
}
# 리팩토링 - State 속성 추가
AbstractParaticipant 추상 클래스 변경
- 플레이어, 딜러 객체는 draw(메소드, 행동)할 때마다 State 상태가 변하면서 사용가능한 행동이 변경/확장되게 된다(OCP원칙준수)
- setState(..) 의 경우 테스트 데이터 초기화시 유용하여 열어 둔다
- Cards가 State 객체 안으로 이동하여서 관리 책임을 위임하였다
- 배율도 State 객체 안으로 이동하여 최종 수익 계산 책임을 위임하였다
public abstract class AbstractParticipant implements Participant {
private String name;
private State state;
private int betAmount;
public AbstractParticipant(String name) {
this.name = name;
this.state = new Hit(new Cards());
this.betAmount = 0;
}
@Override
public void initBetAmount(int betAmount) {
this.betAmount = betAmount;
}
@Override
public void draw(Card card) {
State next = this.state.draw(card);
setState(next);
}
@Override
public void setState(State next) {
this.state = next;
}
@Override
public double profit() {
return this.state.profit(this.betAmount);
}
//..
}
효과1 - State 상태가 추가되면서, 초기 카드를 2장씩 나눠주는 로직이 간소화되었다
변경 전)
private void deal(Deck deck, Dealer dealer, List<Participant> players) {
dealer.draw(deck.draw());
dealer.draw(deck.draw());
for(Participant player : players) {
player.draw(deck.draw());
player.draw(deck.draw());
}
}
변경 후)
private void deal(Deck deck, Dealer dealer, List<Participant> players) {
dealer.setState(StateFactory.create(deck.draw(), deck.draw()));
for(Participant player : players) {
player.setState(StateFactory.create(deck.draw(), deck.draw()));
}
}
효과2 - 최종 수익을 계산하는 로직이 State 객체에 캡슐화가 되어 있기 때문에 Rule 클래스에서는 연산 로직이 제거되고, 그저 호출해서 사용할 수 있게 되었다
AbstractParticipaint 클래스 변경
public abstract class AbstractParticipant {
protected String name;
protected State state;
private int betAmount;
// 생성자, 메소드 ..
@Override
public double profit() {
return this.state.profit(this.betAmount);
}
}
Rule 클래스 변경
public class Rule {
// 필드, 생성자, 메소드
private double judgement(Dealer dealer, Participant player) {
double profit = player.profit(); //*여기
if(isDealerWin(dealer, player)) {
return negativeProfit(profit);
}
if(isPlayerWin(dealer, player)) {
return profit;
}
return 0.0;
}
}
효과3 - 생성자 초기화 대신 setState(Cards)로 초기화하도록 함으로써, 쉽게 초기 설정 가능한 구조를 만들고 가독성 높임
public class RuleTest {
// 다른 케이스..
public static Stream<Arguments> drawDataProvider() {
return Stream.of(
Arguments.arguments("둘 다 블랙잭인 경우", new Blackjack(ACE_KING_BLACKJACK), new Blackjack(ACE_QUEEN_BLACKJACK)),
Arguments.arguments("점수가 같은 경우", new Stay(NINE_KING_19), new Stay(TEN_NINE_19))
);
}
@ParameterizedTest(name = "케이스 : {0}")
@MethodSource("drawDataProvider")
void draw(String title, State dealerState, State plyaerState) {
Dealer dealer = new Dealer();
dealer.setState(dealerState);
Player player = new Player("플레이어");
player.setState(plyaerState);
player.initBetAmount(1000);
Rule rule = new Rule(dealer, Arrays.asList(player));
Map<String, Double> resultMap = rule.getResultMap();
assertThat(resultMap)
.containsEntry(Dealer.DEFAULT_NAME, 0.0)
.containsEntry("플레이어", 0.0);
}
}
회고
3주만에 복습을 했는데, 코드 짜는데 6시간 + 블로그 기록 4시간이 걸렸다 🥺
그래도 되돌아 보니 많은 걸 배운 느낌이 든다
일급 컬렉션, Wrapper 클래스, 상속과 인터페이스, 디자인 패턴
그리고 객체 지향 코드를 작성하는 방법 등등
4만원대 강의이지만 그 이상의 가치를 얻은거 같다
다음에는 코드 리뷰가 포함된 강의를 수강하려고 하는데
피드백까지 얻고, 내가 추가로 얻기 위해 다가간다면
얼마나 더 성장할지 기대가 된다
국비 교육으로 시작해서 SI와 서비스 회사에 다니면서
어떻게 하면 객체 지향적인 코드를 작성할 수 있을지 고민을 많이했지만
방법을 찾지 못해, 매번 일정에 쫓기며 스파게티 코드를 짜왔던 나를 반성하게 해주는
좋은 강의를, 강사분을 만난것에 감사합니다
'공부 > Java' 카테고리의 다른 글
[넥스트스탭] 좌표계산기 - 자바 플레이 그라운드 (3) | 2024.04.30 |
---|---|
[넥스트스탭] 자동차 경주 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[넥스트스탭] 숫자 야구 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[Java] Reflection API 테스트 학습(with Baeldung) (0) | 2023.11.20 |
[Java] Stream Quiz 개인 풀이 (출처. 망나니 개발자 기술 블로그) (0) | 2023.08.15 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!