각 주차의 미션들을 리뷰하면서 어떤 것을 배웠고 부족했던점들은 무엇이 있는지에 대해 정리합니다.
당시 제출 하였던 소스코드는 Github에서 확인하실 수 있습니다.
private static boolean isAces(ArrayList<Card> cards) {
return cards.stream().anyMatch(card -> card.isSymbolEquals(ace));
}
요구사항의 indent depth 1을 만족하기 위해 여러가지 시도를 했었고 스트림의 anyMatch를 사용하여 for문 안의 if 문을 한 줄로 표현 할 수 있었다.
하지만 아직까지 사용에 미숙하다.
정작 중요한 점은 이것이다.
뭔지 잘 알고 있다면 객체지향 프로그래밍은 좋다.
뭔지 잘 알고 있다면 함수형 프로그래밍은 좋다.
뭔지 잘 알고 있다면 함수형 객체지향 프로그래밍은 좋다.Robert C. Martin
그래도 람다와 스트림의 존재를 안것, 그리고 어떤 점에서 좋은지를 알고 배울 수 있게 되었다는것은 나를 조금 더 좋은 학습으로 이끌어 줄 것이라고 생각한다.
public class Player extends User {
private final String name;
private final double bettingMoney;
public Player(String name, double bettingMoney) {
this.name = name;
this.bettingMoney = bettingMoney;
}
@Override
public String getName() {
return name;
}
public double getResultProfit(double moneyRatio) {
return bettingMoney * moneyRatio;
}
}
public class User {
private static final String DEALER_NAME = "딜러 ";
final ArrayList<Card> cards = new ArrayList<>();
public ArrayList<Card> showCards() {
return cards;
}
public void addCard(Card card) {
cards.add(card);
}
public String getName() {
return DEALER_NAME;
}
}
상속은 객체 지향 프로그래밍에서 핵심적인 역할을 한다.
상속을 개념적인 수준에서 이해하는 것과 프로그램을 구현하면서 실제로 적용시키는건 차원이 다른 수준이라고 생각한다.
미션에서 주어진 힌트를 통해 상속에 대해서 공부하였고, 실제로 적용해보면서 공부할 수 있었다.
하지만 아직도 마음에 걸리는건 'User라는 클래스 이름이 적절했는가?'
이다.
'user 패키지에 User라는 클래스가 존재해도 되나?'
가 계속 마음에 걸렸고 Dealer와 Player를 아우르는 이름이 뭘까에 대해서 고민을 많이 했었지만 마땅한 답을 찾지 못했다.
public enum Symbol {
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"),
JACK(10, "J"),
QUEEN(10, "Q"),
KING(10, "K");
private int score;
private String letter;
Symbol(int score, String letter) {
this.score = score;
this.letter = letter;
}
public int getScore() {
return score;
}
public String getLetter() {
return letter;
}
}
Java 문법을 공부하면서 이해는 가지만 막상 사용하기 힘든 것들중 하나가 Enum이였다.
'프리코스 과정 중 한번 정도는 Enum이나 Interface, Abstract Class와 같은 적용 시키기 힘든 클래스들을 연습해 봐야지'
라는 생각을 갖고 있었다.
이번 미션엔 첫 세팅에서 부터 Symbol과 Type 두가지의 Enum이 주어졌고, Enum을 사용해 볼수 있었다.
소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 '연결'하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
Clean Code - Robert C. Martin
public class Application {
public static void main(String[] args) {
BlackJack blackJack = new BlackJack();
blackJack.play();
}
}
class BlackJack {
private ArrayList<Player> players = new ArrayList<>();
private Stack<Card> deck = new Stack<>();
private Dealer dealer = new Dealer();
void play() {
setFirstState();
OutputView.printFirstDistributionMessage(players);
OutputView.printCardsState(dealer, players);
for (Player player : players) {
runAddCardPhase(player);
}
runDealerPhase();
OutputView.printResultState(dealer, players);
OutputView.printResultProfit(dealer, players);
}
private void setFirstState() {
setPlayers();
setDeck();
distributeCards();
}
2주차 미션에서 있었던 것과 같은 문제이다.
플레이어와 덱, 딜러를 먼저 제작하고 BlackJack의 생성자로 넘겨주는 방식이 더 좋은 방식이었을 것이다.
또한 2주차와 마찬가지로 Players 와 Deck (또는 Cards) 와 같은 일급 컬렉션을 사용하기 좋은 상황이었다.
public class NumberHandler {
public static String deleteDecimalPointZero(double number) {
if (Double.toString(number).endsWith(".0")) {
return Double.toString(number).replace(".0", "");
}
return Double.toString(number);
}
}
소수점을 제거하기 위해, Double형을 String으로 바꿔서 대치시키고 있다. 간단히 Math.round() 와 같은 기능을 활용하면 될 문제였다.
java api를 적극 활용한다.
메소드를 직접 구현하기 전에 java api에서 제공하는 기능인지 검색을 먼저 해본다.
java api에서 제공하지 않을 경우 직접 구현한다.- 2주차 공통 피드백
public class Rule {
private static final Card ace = new Card(Symbol.ACE, null);
private static final int BLACKJACK_SCORE = 21;
private static final int BIG_ACE_SCORE = 11;
private static final int SMALL_ACE_SCORE = 1;
private static final int DIFFERENCE_BETWEEN_SMALL_AND_BIG_ACE = BIG_ACE_SCORE - SMALL_ACE_SCORE;
private static final int MIN_DEALER_SCORE = 17;
private static final int FIRST_PLAYER_CARD_COUNTS = 2;
private static final double WINNING_MONEY_RATIO = 1.0;
private static final double LOSING_MONEY_RATIO = -1.0;
private static final double BLACKJACK_MONEY_RATIO = 1.5;
private static final double TIE_MONEY_RATIO = 0.0;
private static final int BURST_SCORE = -1;
public static int getScore(ArrayList<Card> cards) {
int score = cards.stream().mapToInt(Card::getScore).sum();
if (score > BLACKJACK_SCORE) {
return BURST_SCORE;
}
if (isAces(cards) && score <= BLACKJACK_SCORE - DIFFERENCE_BETWEEN_SMALL_AND_BIG_ACE) {
return score + DIFFERENCE_BETWEEN_SMALL_AND_BIG_ACE;
}
return score;
}
private static boolean isAces(ArrayList<Card> cards) {
return cards.stream().anyMatch(card -> card.isSymbolEquals(ace));
}
public static boolean canDrawCardMore(Player player) {
int playerScore = getScore(player.showCards());
return playerScore < BLACKJACK_SCORE && playerScore != BURST_SCORE;
}
public static boolean isDealerDrawCard(Dealer dealer) {
int dealerScore = getScore(dealer.showCards());
return dealerScore < MIN_DEALER_SCORE && dealerScore != BURST_SCORE;
}
public static double getDealerProfit(Dealer dealer, ArrayList<Player> players) {
double dealerProfit = TIE_MONEY_RATIO;
for (Player player : players) {
dealerProfit -= getPlayerProfit(dealer, player);
}
return dealerProfit;
}
public static double getPlayerProfit(Dealer dealer, Player player) {
int dealerScore = getScore(dealer.showCards());
int playerScore = getScore(player.showCards());
if (dealerScore == BURST_SCORE || isBlackJack(dealer) || isBlackJack(player)) {
return calculateProfitInAbnormalCase(dealer, dealerScore, player);
}
return calculateProfit(player, playerScore, dealerScore);
}
private static boolean isBlackJack(User user) {
int userScore = getScore(user.showCards());
return userScore == BLACKJACK_SCORE && user.showCards().size() == FIRST_PLAYER_CARD_COUNTS;
}
private static double calculateProfitInAbnormalCase(Dealer dealer, int dealerScore, Player player) {
if (dealerScore == BURST_SCORE) {
return calculateProfitInDealerBurst(player);
}
if (isBlackJack(dealer)) {
return calculateProfitInDealerBlackJack(player);
}
return player.getResultProfit(BLACKJACK_MONEY_RATIO);
}
private static double calculateProfit(Player player, int playerScore, int dealerScore) {
if (playerScore > dealerScore) {
return player.getResultProfit(WINNING_MONEY_RATIO);
}
if (playerScore == dealerScore) {
return TIE_MONEY_RATIO;
}
return player.getResultProfit(LOSING_MONEY_RATIO);
}
private static double calculateProfitInDealerBurst(Player player) {
if (isBlackJack(player)) {
return player.getResultProfit(BLACKJACK_MONEY_RATIO);
}
if (getScore(player.showCards()) == BURST_SCORE) {
return player.getResultProfit(LOSING_MONEY_RATIO);
}
return player.getResultProfit(WINNING_MONEY_RATIO);
}
private static double calculateProfitInDealerBlackJack(Player player) {
if (isBlackJack(player)) {
return TIE_MONEY_RATIO;
}
return player.getResultProfit(LOSING_MONEY_RATIO);
}
}
강조하는 차원에서 한 번 더 말하겠다.
큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.Clean Code - Robert C. Martin
Rule에선 Score를 구하고, 플레이어 또는 딜러가 카드를 더 뽑을 수 있는지 검사하고 그들의 수익까지 계산해주는 만능 클래스다.
당연히, 만능 클래스는 옳지 않다.
Rule에선 Score를 계산해주고 (A를 1 또는 11로 처리, J,Q,K를 10으로 처리해주는 게임의 규칙)
플레이어와 딜러가 Score를 계산 받은뒤 카드를 더 뽑을 수 있는지 스스로 검사하고
수익은 수익을 계산해주는 클래스를 만들거나 게임을 진행하는 클래스에서 계산해주는게 훨씬 더 실세계와 닮아 있고 이해하기 쉬운 구조일 것이라고 생각한다.