Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[블랙잭 1단계] 모찌 미션 제출합니다. #124

Open
wants to merge 67 commits into
base: wondroid-world
Choose a base branch
from

Conversation

wondroid-world
Copy link

@wondroid-world wondroid-world commented Mar 7, 2025

셀프 체크리스트

  • 프로그램이 정상적으로 작동하는가?
  • 모든 테스트가 통과하는가?
  • 이전에 받은 피드백을 모두 반영하였는가?
  • 코딩 스타일 가이드를 준수하였는가?
    • IDE 코드 자동 정렬을 적용하였는가?
    • 린트 검사를 통과하였는가?
  • 프로그래밍 요구 사항을 준수하였는가?
  • README.md에 기능 목록을 정리하고 명확히 기술하였는가?

어떤 부분에 집중하여 리뷰해야 할까요?

  1. 객체가 메시징을 던진다.
    이 의미를 어떻게 판단해야 되는 지 고민을 해봤습니다. 처음에는 객체는 메시징을 던진다라는 말을 들었을 때, 다른 객체에서 완전히 원하는 포맷의 주어야한다고 생각을 했습니다.
 fun getCardsInfomation(): String = value.map { it.combine().joinToString() }

카드들의 정보를 출력할 때 List 형태가 아닌 OutputView에서 출력하기 편한 String 타입으로 반환하는 것이 더 좋다고 생각했습니다. 하지만, 객사오를 읽으면서 객체는 자율적으로 행동한다라는 문장을 읽으면서 완전히 상대방이 원하는 값을 주는 것이 아니라, 상태에 대한 메시징을 던지는 것이 중요하다고 생각했습니다.

 fun getCardsInfomation(): List<String> = value.map { it.combine() }

그래서 위와 같이 수정을 했습니다. 메시징을 던진다에 대해서 저의 생각의 변화에 대해서, 페로로의 생각이 궁금해요!

  1. 어디까지 확장성을 생각해야할까요

저는 설계를 할 때 확장성을 많이 고려하는 편입니다. 카드를 생성할 때도 모든 카지노에서 쓸 수 있도록 아래와 같이 구현했습니다.

package blackjack.model

enum class Denomination(
    val title: String,
    val number: Int,
) {
    ACE("A", 1),
    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("J", 11),
    QUEEN("Q", 12),
    KING("K", 13),
}

하지만, 이렇게 확장성을 고려하다보니, 블랙젝에서 구현하는 데, 블랙잭 점수에 대해서 너무 많은 분기를 처리해야하더라구요. 그래서, 아래와 같이 블랙잭에 핏한 카드들을 생성했습니다.

package blackjack.model

enum class Denomination(
    val title: String,
    val number: Int,
) {
    ACE("A", 0),
    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("J", 10),
    QUEEN("Q", 10),
    KING("K", 10),
}

일단 현실에 주어진 것을 구현한 다음, 확장성을 고려해야할까요? 둘 중 어떤게 선행이 되어야 하는 지 궁금합니다.

wondroid-world and others added 30 commits March 4, 2025 17:00
- 카드 모양 추가
- 카드는 모양과 끗수를 가진다.
- 카드의 끗수에 대한 요구 사항 추가
- 응답에 대한 플레이어 행동 반환
- 이름, 카드 리스트 가짐
wondroid-world and others added 29 commits March 6, 2025 15:16
- 딜러의 hit 과 stay 로직 제외 구현
Copy link

@laco-dev laco-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1단계 고생 많으셨어요.

여러가지 고민과 미래를 그리는 모습이 보이는데요!
기본적인 개념과 연습이 충분히 된 다음에 고민을 해야 할 요소들로 보여요.
아직은 잘 모르는데도 불구하고 오버 엔지니어링을 하려는 것으로 느껴지기도 합니다.

점진적으로 개선해볼 요소들이 있으니 우선은 관심사 분리부터 연습해 보도록 하시죠.

) {
fun isDenominationAce(card: Card): Boolean = card.denomination == Denomination.ACE

fun combine(): String = denomination.title + shape.koreanName
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체가 메시징을 던진다.
이 의미를 어떻게 판단해야 되는 지 고민을 해봤습니다. 처음에는 객체는 메시징을 던진다라는 말을 들었을 때, 다른 객체에서 완전히 원하는 포맷의 주어야한다고 생각을 했습니다.

기본적으로 객체가 꽁꽁 숨겨져있고, 객체로 하여금 필요한 행동을 메시징 이라는 형태로 요청한다고 봐주시면 됩니다.
그렇기에 객체가 스스로 할 수 있도록 자율성을 부여할 수 있게 됩니다.

그래서 위와 같이 수정을 했습니다. 메시징을 던진다에 대해서 저의 생각의 변화에 대해서, 페로로의 생각이 궁금해요!

다만 위 예시는 메시징을 통해 가져오는 것과 어울리지 않습니다.
왜냐하면 어떻게 출력할 지 결정하는 것은. Card로부터 필요한 기능이 아니기 때문인데요.

가령 2하트 라고출력하는 것을 화면에 따라 ❤️2 이렇게 출력할지도 모릅니다.
혹은 그림으로 출력할수도 있죠.

메시징을 던지기 이전의 관심사 분리가 선행되어야 할 것으로 보입니다!

val shape: CardShape,
val denomination: Denomination,
) {
fun isDenominationAce(card: Card): Boolean = card.denomination == Denomination.ACE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자기 스스로가 Card인데 파라미터로 Card를 받는건 무엇인가요?

Comment on lines +7 to +19
ACE("A", 0),
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("J", 10),
QUEEN("Q", 10),
KING("K", 10),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하지만, 이렇게 확장성을 고려하다보니, 블랙젝에서 구현하는 데, 블랙잭 점수에 대해서 너무 많은 분기를 처리해야하더라구요. 그래서, 아래와 같이 블랙잭에 핏한 카드들을 생성했습니다.

이미 enum에 점수를 넣은 것 부터 점수를 결정하는 규칙을 도메인에서 결정하였다고 보입니다.
점수가 동적으로 결정될 것을 고민하는 확장성이라면 애초에 enum에 포함되는 것 자체가 모순이겠죠?

다만 지금 블랙잭 규칙에서는 그러한 요구사항이 없기 때문에 enum에 포함되어도 괜찮다고 봅니다.
확장성을 고려하는 것은 중요하지만 과한 확장성은 객체의 복잡도를 크게 높이거나 오버 엔지니어링이 될 위험이 있습니다.

Comment on lines +6 to +9
DIAMOND("다이아몬드"),
SPADE("스페이드"),
HEART("하트"),
CLOVER("클로버"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지로 카드 모형이 위와 같이 출력될 것이라는 것을 결정하는 것은 도메인이 아닌 UI 의 관심사입니다. 😓

Comment on lines +6 to +22
HIT("Y"),
STAY("N"),
;

companion object {
fun from(answer: String): PlayerBehavior =
when (answer) {
"Y" -> HIT
"N" -> STAY
else -> throw IllegalArgumentException("ddd")
}

fun from(dealerScore: Int): PlayerBehavior {
if (dealerScore <= 16) return HIT
return STAY
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이어서 플레이어가 어떻게 행동할지 또한, "Y" "N"으로 결정되는 것이 아닌 Hit Stay로 결정되어야 합니다.
만약 화면에서 진행하는 게임이었다면 유저는 "Y"라고 타이핑 하지 않고 버튼을 클릭합니다.

도메인 모델이 UI 패키지에 의존만 하지 않으면 괜찮다는 뜻이 아니에요.
UI와 관심사를 나누는게 근본적인 목적입니다.

_value.add(card)
}

fun isBlackjack(firstTurn: Boolean): Boolean = CardsStatus.from(calculateScore(), firstTurn) == CardsStatus.BLACKJACK
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

블랙잭 상태는, 첫번째 턴이라기 보다는 "카드가 2장일 때 합계가 21점이면" 으로 접근을 하시는 방향이 좋을거에요!

Comment on lines +20 to +30
fun calculateScore(): Int {
val aceCount: Int = value.count { card -> card.isDenominationAce(card) }
val score: Int = value.sumOf { card -> card.denomination.number }
return when (aceCount) {
1 -> if (score < 11) score + 11 else score + 1
2 -> if (score < 10) score + 11 + 1 else score + aceCount * 1
3 -> if (score < 9) score + 11 + 1 + 1 else score + aceCount * 1
4 -> if (score < 8) score + 11 + 1 + 1 + 1 else score + aceCount * 1
else -> score
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

알고리즘적이 조금 복잡하게 느껴지는데 쉽게 생각해보세요!

카드뭉치에 에이스 카드가 있을 때 .... 보너스 점수 10점을 획득한다.

Comment on lines +12 to +21
fun of(
standardScore: Int,
comparedScore: Int,
): GameResult {
when {
standardScore == comparedScore -> return PUSH
standardScore > comparedScore -> return WIN
}
return LOSE
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

블랙잭 규칙으로 보면, 21점이 넘는다거나 하면 무조건 패배가 됩니다.

참가자 입장에서 승부가 벌어지고 있는데 단순히 점수만으로 추가로 결정하기에는 로직이 분산되어 있는 느낌이 들기도 합니다.
가령 점수가 높더라도 질 수도 있으니까요.

Comment on lines +3 to +6
open class Player(
open val name: String,
open val cards: Cards = Cards(emptyList()),
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open class, abstract class, interface의 차이점을 알고 계신가요?
플레이어 그리고 딜러는 어떤 관계로 나타내야 적절할까요?

가령 딜러와 플레이어는 동일한 로직을 가지지만 일부는 서로 다른 로직을 수행하고는 합니다.
이를 표현하기 위한 적절한 방법은 무엇일까요?
아래 힌트를 참고해보세요 :)

#1
ParticipantPlayerDealer

#2
PlayerDealer

Comment on lines +33 to +37
fun from(name: String): Player {
require(name != "딜러") { "플레이어는 딜러라는 이름을 가질 수 없습니다." }
require(name.length in 1..5) { "플레이어는 1에서 5사이 길이의 이름만 가질 수 있습니다." }
return Player(name)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한편 지금 상속 구조라면 딜러 == 플레이어인데 플레이어가 딜러 이름을 가질 수 없다는 점이 매우 모순적으로 느껴지기는 합니다...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants