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단계] 뭉치 미션 제출합니다. #122

Open
wants to merge 66 commits into
base: m6z1
Choose a base branch
from

Conversation

m6z1
Copy link

@m6z1 m6z1 commented Mar 7, 2025

안녕하세요 둘리!
모바일 안드로이드 크루 뭉치입니다 ㅎㅎ
이렇게 만나뵙게 되어 영광입니다 😄🍀

블랙잭 미션을 돌아가게하자! 라는 부분에 초점을 두어, 코드가 질서정연하지 않고 가독성이 떨어지는 것 같습니다..
따끔하게 피드백 주시면 감사하겠습니다!!

이번 블랙잭 미션 잘 부탁드려요!!🙇‍♀️

셀프 체크리스트

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

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

1. 모든 객체를 설계하고 컨트롤러 및 뷰를 생성하다보니 생각보다 컨트롤러가 하는 역할이 많다? 라는 생각이 들었습니다.

저는 기존에 controller에서 객체를 생성해도 되며, 서비스의 흐름을 가지는 것이 컨트롤러의 역할이라고 생각해 블랙잭 게임의 흐름을 모두 컨트롤러에서 구현했습니다. 하지만, 이번 블랙잭 미션에서 느낀 문제점이 있었습니다.

단위 테스트만 진행하다보니 여러 가지의 분기에 대해서 테스트를 할 수 없다는 문제점을 깨달았습니다. 특히나 블랙잭 미션의 경우에는 딜러가 ~하면, 플레이어가 ~한 경우의 분기가 굉장히 많다보니 실제로 그 플로우를 테스트하기까지 application을 계속 돌려야 하는 문제가 있었습니다.

그렇다면 블랙잭 게임을 주관하는 새로운 도메인 객체를 만들어야 할까? 라는 생각도 들었는데 이것에 대해서는 어찌 생각하시는지 궁금합니다!
이번 피드백에서 만들어보라고 하시면 반영하고 둘의 장단점을 느껴보도록 하겠습니다!! ㅠㅠ

다른 내용은 코멘트로 달겠습니다!

wondroid-world and others added 30 commits March 4, 2025 17:00
- 카드 모양 추가
- 카드는 모양과 끗수를 가진다.
- 카드의 끗수에 대한 요구 사항 추가
- 응답에 대한 플레이어 행동 반환
- 이름, 카드 리스트 가짐
m6z1 and others added 26 commits March 6, 2025 15:34
- 딜러의 hit 과 stay 로직 제외 구현
@m6z1 m6z1 changed the base branch from main to m6z1 March 7, 2025 08:40
Copy link
Author

@m6z1 m6z1 left a comment

Choose a reason for hiding this comment

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

안녕하세요 둘리!

고민이 많았던 부분에 대해서 코멘트를 남겨두었습니다.

이번 미션 잘 부탁드립니다 : )

package blackjack.model

class Cards(
value: List<Card>,
Copy link
Author

Choose a reason for hiding this comment

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

기존의 cards의 생성자 네이밍은 cards 였습니다.
일급컬렉션을 만들어 외부에서 사용하게 되면 , List<Card>에 접근하기 위해서는
cards.cards.~~로 사용을 했어야 했는데, 미관상으로도 그렇고 카드들의 카드들이라는 이름이 어색하다고 느꼈습니다.
이에 value라는 네이밍을 사용했는데, 조금 추상적인가? 라는 생각도 들었습니다.
둘리는 어떻게 생각하시는지 궁금합니다..!!

Choose a reason for hiding this comment

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

취향차이라고 생각하는데, 저는 cards.cards를 더 많이 사용하긴 합니다. 정답은 없어요!

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

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

Choose a reason for hiding this comment

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

combine이란 함수가 String 을 반환하는데, 다시 생각해보니 view에서 결합하여 출력을 하는 게 더 적절할 것이라는 생각이 들었습니다.

A다이아몬드에서 다이아몬드A로 출력 요구 사항이 변경될 경우, model이 아닌 view에서 변경을 해야 하지 않나?
하지만, 반대로 생각하면 card에서 메시지를 받아 반환 할 수 있는 것은 아닐까? 라는 생각이 들었습니다...

고민의 끝에 현재 코드는 card 에서 직접 제작해서 제출하고 있습니다.

Choose a reason for hiding this comment

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

combine 함수의 로직은 뷰의 관심사가 맞습니다.
그럼 이렇게 생각해보면 어떨까요?

  1. 이 도메인 로직을 여러 플랫폼에서 사용한다
  2. combine() 함수로 뷰에 출력할 String값을 받는다.
  3. A플랫폼에서는 A다이아몬드, B플랫폼에서는 B다이아몬드 라고 출력해야한다.

뷰의 요구사항이 바뀌면 뷰를 수정하는 것이 자연스럽지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

현재 Dealer는 Player를 상속받고 있습니다.
하지만, dealer가 아닌 player들은 딜러 라는 이름을 가질 수 없다고 판단했습니다.

하지만, init을 통해 이름에 대한 검증을 진행하게 되면, 딜러 또한 이 에러에 막히는 것을 깨달았습니다.

그래서 dealer 가 아닌 player의 경우에는 팩토리 함수를 통해 객체를 생성하며 이름을 검증할 수 있도록 했고,
dealer의 경우에는 player의 생성자를 통해 직접적으로 객체를 생성할 수 있도록 했습니다.

하지만, 생성자는 public 으로 열려있어서 누구든지 Player("이름")을 통해 객체를 생성할 수 있습니다.
dealer 가 아닌 player 는 생성자로 접근해 Player 객체를 만들 수 있으니 그렇다면 팩토리 메서드가 의미가 있는 것일까? 라는 생각이 들었습니다.

어떻게 문제를 해결해야 할 지 방향성을 제시해주시면 감사하겠습니다 ㅠㅠ!!

Choose a reason for hiding this comment

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

생성자가 public으로 열려있더라도, 생성자 외에 다른 의도를 주고 싶다면 팩토리 함수를 사용해도 이상하지 않다고 생각해요.
하지만 요구사항에 대한 검증은 init에 두는 것이 자연스럽다고 생각합니다. 말씀하신대로 누구든지 Player("이름")을 통해 객체를 생성할 수 있기 때문이에요.

플레이어와 딜러에게 다른 요구사항을 검증하고 싶다면, 딜러가 플레이어를 상속하는 것이 아니라
딜러-플레이어를 공통화할 수 있는 새로운 상위 클래스를 생성하고, 딜러와 플레이어가 그 클래스를 상속받는 형태는 어떨까요?

추가로 플레이어 이름이 겹치면 안된다는 요구사항은 없는데요. 🤔 다른 이야기일 수 있겠지만 동명이인이라면요..?🥹

Copy link

@hyemdooly hyemdooly left a comment

Choose a reason for hiding this comment

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

안녕하세요, 뭉치! 이번 미션을 함께 하게 된 둘리입니다. 반가워요! 😄
객체를 많이 나눠주셨어요! 여기서 좀 더 어떤 객체가 어떤 정보를 가지고 있어야 할지, 어떤 행위를 해주어야할지 고민해보면 좋겠어요.
질문에 대한 답과 함께 고민해볼만한 내용들을 코멘트로 달아드렸습니다.
더 궁금한 점이 있으시면 코멘트 달아주시거나 DM 주세요! 💪

import blackjack.view.InputView
import blackjack.view.OutputView

class BlackjackController(

Choose a reason for hiding this comment

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

딜러가 ~하면, 플레이어가 ~한 경우의 분기가 굉장히 많다보니

혹시 이런 경우의 테스트가 어떤 것이 있을까요?
보통은 단위 테스트면 충분하다고 생각하고, 전제조건을 초기값으로 제공해서 테스트하는 함수들이 지금도 보여서요!

Choose a reason for hiding this comment

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

별개로 지금도 컨트롤러가 하고있는게 많아보이긴하는데... 우선 다른 것에 집중해서 수정해보고, 그 이후에 컨트롤러를 살펴보죠!

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

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

Choose a reason for hiding this comment

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

combine 함수의 로직은 뷰의 관심사가 맞습니다.
그럼 이렇게 생각해보면 어떨까요?

  1. 이 도메인 로직을 여러 플랫폼에서 사용한다
  2. combine() 함수로 뷰에 출력할 String값을 받는다.
  3. A플랫폼에서는 A다이아몬드, B플랫폼에서는 B다이아몬드 라고 출력해야한다.

뷰의 요구사항이 바뀌면 뷰를 수정하는 것이 자연스럽지 않을까요?

Choose a reason for hiding this comment

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

생성자가 public으로 열려있더라도, 생성자 외에 다른 의도를 주고 싶다면 팩토리 함수를 사용해도 이상하지 않다고 생각해요.
하지만 요구사항에 대한 검증은 init에 두는 것이 자연스럽다고 생각합니다. 말씀하신대로 누구든지 Player("이름")을 통해 객체를 생성할 수 있기 때문이에요.

플레이어와 딜러에게 다른 요구사항을 검증하고 싶다면, 딜러가 플레이어를 상속하는 것이 아니라
딜러-플레이어를 공통화할 수 있는 새로운 상위 클래스를 생성하고, 딜러와 플레이어가 그 클래스를 상속받는 형태는 어떨까요?

추가로 플레이어 이름이 겹치면 안된다는 요구사항은 없는데요. 🤔 다른 이야기일 수 있겠지만 동명이인이라면요..?🥹

Comment on lines +9 to +11
open fun appendCard(card: Card) {
cards.add(card)
}

Choose a reason for hiding this comment

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

Controller에서 deck에서 카드를 뽑아서 주지말고, player가 직접 deck에서 카드를 뽑는다면 어떨까요?

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
}
}

Choose a reason for hiding this comment

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

이 계산 로직이 이렇게 길지 않아도 해결할 수 있을 것 같아요. 🤔

private val _value: MutableList<Card> = value.toMutableList()
val value: List<Card> get() = _value.map { card -> card.copy() }

var size: Int = this.value.size

Choose a reason for hiding this comment

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

이 변수는 테스트를 위함일까요? 🤔

open val name: String,
open val cards: Cards = Cards(emptyList()),
) {
lateinit var result: GameResult

Choose a reason for hiding this comment

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

사실 저는 이 result가 왜있는지 잘 모르겠어요!
result는 결과 계산이 필요할 때만 계산해서 보여주면 될 것 같은데, 게임 진행 내내 갱신을 하고 있더라구요.
진행 로직 정리하면서 이 result가 꼭 필요한지 고민해보면 좋겠어요.

Comment on lines +13 to +14
"Y" -> HIT
"N" -> STAY

Choose a reason for hiding this comment

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

어차피 하드코딩된 "Y"와 "N"에 따라 결과값을 리턴할거라면 enum 내의 answer는 정의하지 않아도 괜찮아보여요.
파라미터 answer가 지금 String인데, 진행 여부를 Boolean으로 받아도 충분하지 않을까요?

Comment on lines +16 to +18
fun isBlackjack(firstTurn: Boolean): Boolean = CardsStatus.from(calculateScore(), firstTurn) == CardsStatus.BLACKJACK

fun isBust(): Boolean = CardsStatus.from(calculateScore()) == CardsStatus.BUST

Choose a reason for hiding this comment

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

상태를 활용하겠다면, Cards가 상태를 가지고, 카드가 추가될 때마다 상태를 업데이트하는 것이 자연스럽지 않을까요?

Comment on lines +4 to +13
private val DENOMINATIONS: List<Denomination> = Denomination.entries
private val SHAPES: List<CardShape> = CardShape.entries

val CARDS: List<Card> =
DENOMINATIONS
.flatMap { denomination ->
SHAPES.map { shape ->
Card(shape, denomination)
}
}.shuffled()

Choose a reason for hiding this comment

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

Kotlin에서 이렇게 전부 대문자로 이름을 짓는 경우는 어떤 경우일까요?

@hyemdooly
Copy link

추가로 전체적인 블랙잭 로직을 확인해보면 좋겠어요.

딜러카드: 3다이아몬드, A하트, Q다이아몬드, 2하트, 3스페이드 - 결과: 19
a카드: A클로버, 5클로버, K다이아몬드 - 결과: 16
b카드: 5하트, 10클로버 - 결과: 15
c카드: 8하트, 3하트 - 결과: 11

## 최종 승패
딜러: 3무
a: 무
b: 무
c: 무

위 경우는 딜러가 이겨야합니다. 딜러가 Bust가 아니면서 21 숫자에 가깝기 때문이에요.

image

제가 아는 블랙잭 규칙으로는 플레이어들이 모두 카드뽑기가 끝나면 딜러가 카드를 더 뽑는 것으로 알고 있는데요. 지금은 한 명의 플레이어가 카드를 뽑으면 딜러가 뽑고 있어요.
사실 딜러가 먼저 Bust해버린다면 플레이어들은 카드를 더 뽑을 필요 없습니다. 아직 Bust하지 않은 모든 플레이어가 승리하기 때문이에요.

전체적으로 로직을 한번 체크해보시면 좋을 것 같아요! 객체 간의 역할과 책임을 고민하다보면 컨트롤러 로직도 좀 더 단순화되지 않을까 싶네용 💪

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