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

[2단계 - 블랙잭 베팅] 리비(이근희) 미션 제출합니다. #709

Merged
merged 100 commits into from
Mar 17, 2024

Conversation

Libienz
Copy link

@Libienz Libienz commented Mar 14, 2024

안녕하세요 썬 ☀️
2단계 PR 요청 드립니다.

2단계에서는 어떤 객체가 어떤 속성을 가지고 있고 어떤 기능을 제공해야 하는지를 중점적으로 고민해봤습니다.
제가 제일 약한 부분이더군요... 쉽사리 답을 찾지 못하고 여기저기 책임을 퍼뜨린 것 같습니다.

2단계도 잘 살펴봐주시면 열심히 학습해보겠습니다
감사합니다 😊

@Libienz
Copy link
Author

Libienz commented Mar 15, 2024

안녕하세요 썬 ☀️

2단계 리팩토링 진행해보았습니다.
과한 분리를 줄이고 도메인에 역할들을 할당함으로써 명세를 자세히 드러내보고자 노력했어요 🤔
다만 그런 의도가 잘 드러나는지는 확신이 없네요 🫠

여러 질문 주세요 썬!
나름(?) 깊게 고민하고 작성한 코드라 썬이랑 여러 의견 나눠보고 싶어요 🙌

이번에도 잘 부탁드립니다 🙇🏻‍♂️

Copy link

@syoun602 syoun602 left a comment

Choose a reason for hiding this comment

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

안녕하세요 리비~

리팩토링 이후에 훨씬 깔끔한 코드를 작성하셨네요! 💯
코멘트 남겨두었으니 확인 부탁드려요.
또한, 중점적으로 살펴봤으면 하는 부분이 있다면 해당 코드에 PR 코멘트를 남겨주시면 됩니다.
어느 부분에서 어떤 고민을 하셨는지 말씀해주세요 😊

@@ -0,0 +1,24 @@
package blackjack.domain.bet;

public class BetAmout {

Choose a reason for hiding this comment

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

Suggested change
public class BetAmout {
public class BetAmount {

Comment on lines 10 to 14
BLACKJACK_WIN(1.5, (dealer, player) -> dealer.hasNoBlackJackHand() && player.hasBlackJackHand()),
PLAYER_LOSE(-1.0, (dealer, player) -> player.isBusted() || dealer.hasScoreAbove(player)),
PLAYER_WIN(1.0, (dealer, player) -> (player.isNotBusted() && (dealer.isBusted() || player.hasScoreAbove(dealer)))),
PUSH(0.0, (dealer, player) -> player.isNotBusted() && dealer.isNotBusted() && player.hasSameScore(dealer));

Choose a reason for hiding this comment

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

predicate를 활용하셨네요 👍 이전 if문으로 분기하는 것에서 현재로 변경하신 이유가 궁금합니다~
또한, 리비가 보시기엔 가독성 측면에서 어떤 코드가 더 좋은가요?

Copy link
Author

Choose a reason for hiding this comment

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

가독성 측면에서는 비슷하다고 판단합니다! 조건식이 거의 동일한 것 같아서요 🤔

제가 위의 코드처럼 바꾼 이유는 채점 로직 분리를 통해 Judge 클래스를 파생되게 만드는 것보다 어떤 결과가 나올지 판정 로직을 GameResult 도메인 안에 응집시키고 싶었기 때문입니다.

다만 다음과 같은 고민이 있기는 합니다.
플레이어가 승리하는 판정 로직을 변경해주세요라는 변경 요구사항이 들어오면 다른 개발자들이 위화감없이 GameResult라는 enum을 찾아올 수 있을지가 고민이에요!

관련해서 썬의 의견도 들어보고 싶습니다 🙌

return Arrays.stream(values())
.filter(gameResult -> gameResult.biPredicate.test(dealer, player))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("[INTERNAL ERROR] 게임 결과를 판정할 수 없습니다"));

Choose a reason for hiding this comment

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

사소하지만 internal error라면 IllegalArgumentException일까요?

Copy link
Author

Choose a reason for hiding this comment

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

파라미터가 적절하지 않다는 의미에서 IllegalArgumentException을 생각하기는 했어요 🤔
다만 NoSuchElement등 다른 예외도 생각이 나기는 하네요

표준 예외를 사용한다고 했을 때 어떤 예외를 고르는지 팁이 있을까요!?
골라본 경험이 많지 않다보니 이것도 될 것 같고 저것도 될 것 같고 많이 헷갈립니다 🤔

@@ -19,37 +19,34 @@ public static Hand createHandFrom(CardDeck cardDeck) {
return new Hand(cardDeck.popCards(INITIAL_HAND_SIZE));
}

public Score calculateScore() {
int sum = calculateCardSummation();
if (hasAce() && (sum + ACE_WEIGHT) <= BUST_THRESHOLD) {

Choose a reason for hiding this comment

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

Suggested change
if (hasAce() && (sum + ACE_WEIGHT) <= BUST_THRESHOLD) {
if (hasAce() && isNotBust()) {

이런식으로 메서드 추상화 수준을 맞춰주면 코드 가독성이 더 좋습니다

Copy link
Author

Choose a reason for hiding this comment

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

새로운 시각 알게 되었습니다 💪🏻

public static GameResult of(Dealer dealer, Player player) {
return Arrays.stream(values())
.filter(gameResult -> gameResult.biPredicate.test(dealer, player))
.findFirst()

Choose a reason for hiding this comment

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

순서에 상관이 없다면 findAny를 사용할 수 있습니다
각각에 대한 차이를 알아보아요~

Copy link
Author

Choose a reason for hiding this comment

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

스트림 요소들을 직렬로 처리할 때는 문제가 없지만 병렬로 처리하는 경우에 차이가 있을 수 있다는 것을 학습할 수 있었습니다 👊


GameResult(double profitLeverage) {
GameResult(double profitLeverage, BiPredicate<Dealer, Player> biPredicate) {

Choose a reason for hiding this comment

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

이렇게 두면 접근 제어자가 어떻게 될까요?

Copy link
Author

Choose a reason for hiding this comment

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

열거형 생성자는 기본적으로 접근제어자가 private인걸로 학습했습니다!
public, protected로 설정해보았는데 not allowed here라고 하면서 빨간줄이 뜨더라고요 🤔

불필요한 생성을 막기 위한 맥락으로 이해했습니다 👍

this.biPredicate = biPredicate;
}

public static GameResult of(Dealer dealer, Player player) {

Choose a reason for hiding this comment

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

이 메서드는 생성자인가요?

Copy link
Author

Choose a reason for hiding this comment

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

생성자가 아닙니다. 다만 GameResult.of(dealer, player)와 같은 코드가 딜러와 플레이어의 게임 결과처럼 평문적으로 읽힌다고 생각해서 네이밍을 위처럼 진행해보았어요.

썬의 말씀 듣고 추가적인 생각해보았습니다 🤔
그 결과 다른 개발자가 본다면 정적 팩토리 메서드의 네이밍 패턴을 사용하고 있기에 헷갈릴 수도 있겠다는 생각을 하게 되었어요.

고쳐보는게 낫다고 판단하게 되었습니다. 반영해볼게요 💪🏻


PlayerProfits playerProfits = playerBets.calculateProfitResult(dealer);

assertThat(GameResult.of(dealer, player2)).isEqualTo(GameResult.PLAYER_LOSE);

Choose a reason for hiding this comment

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

assertAll에 포함되지 않은 이유가 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

앗.. 테스트가 통과하지 않아서 디버깅 용도로 제가 작성한 테스트인데 삭제하지 않았네요 😓

@DisplayName("특정 플레이어의 수익을 조회할 수 있다")
@Test
void testFindProfitOfPlayer() {
Player player1 = TestPlayerCreator.of("썬", 1, 2, 3, 4);

Choose a reason for hiding this comment

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

엇 저도 있었군여 🙇‍♂️

Player player1 = TestPlayerCreator.of("썬", 1, 2, 3, 4);
Player player2 = TestPlayerCreator.of("리비", 3, 4);

Map<Player, Profit> playerProfitMap = new HashMap<>();

Choose a reason for hiding this comment

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

기본적으로 변수명에는 자료구조를 사용하지 않습니다~
클린코드의 의미 있는 이름에 대해 학습해보아요

Suggested change
Map<Player, Profit> playerProfitMap = new HashMap<>();
Map<Player, Profit> playerProfits = new HashMap<>();

Copy link
Author

Choose a reason for hiding this comment

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

특정 자료형에 종속적인 변수명은 의미를 드러내지 못하고 확장사항에 잘 견디지 못하는 것을 학습할 수 있었습니다
반영해보겠습니다 💪🏻

@Libienz
Copy link
Author

Libienz commented Mar 17, 2024

안녕하세요 썬 ☀️
코멘트 반영 위주의 리팩토링 진행해보았습니다.

미션 진행에 대해서 다음의 것들에 대해 썬과 의견 나눠보고 싶어요 🙌
썬이 생각 들려주신다면 매우 큰 도움이 될 것 같습니다 😃

🙋🏻‍♂️ 연관관계 방향에 대해서 고민해보았습니다

저는 플레이어가 베팅금액을 가지지 않도록 설계했어요 🤔

처음에는 플레이어 도메인에 속성으로써 추가하려고 했는데 이렇게 한두개씩 플레이어에 추가하다가는 후에 Player가 가지게 되는 속성이 굉장히 많아질수도 있다고 생각하게 되었습니다.
그래서 플레이어가 베팅금액을 가지지 않도록 구현하기로 하였고 결과적으로 매핑하는 클래스를 추가로 구현했어요 🤔

현업의 경우를 저 혼자 멋대로 상상해보았는데요 현업의 도메인들은 지금의 미션과는 비교도 안될 정도로 많은 연관관계를 가지고 있을 것 같아요.
보통의 경우 도메인 연관관계를 어떤 방향으로 관리하는지 알려주실 수 있을까요? (플레이어가 Bet을 가지고 있는지, Bet이 플레이어를 알고 있는지, 매퍼가 있는지)

학습할만한 키워드 짧게 던져만주셔도 도움 많이 될 것 같습니다 💪🏻

🙋🏻‍♂️ 도메인은 어떤 속성을 가지고 어떤 속성을 외부로 덜어내야 하는가

위 질문과 비슷한 맥락입니다.

플레이어가 플레이어 이름을 속성으로 가지고 있는 것은 제게는 매우 합리적입니다.
플레이어가 자신의 손패를 속성으로 가지고 있는 것 또한 그렇고요 .
사실 플레이어가 자신의 베팅을 속성으로 가지고 있는 것 또한 제게 위화감이 없습니다.

이렇게 치면 플레이어 관련된 모든 속성이 플레이어에 엮여야 하는데 이는 결국 클래스를 비대하게 만들고 시스템 유지보수의 취약점이 될 수 있을 것 같다는 생각입니다.

어디까지를 플레이어가 알고 어디까지가 외부로 덜어내져야 하는 것일까요 .. 절대적 기준이 없음을 알지만 썬의 의견이 도움이 될 수 있을 것 같습니다 🙇🏻‍♂️

(지금 생각해보니 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. 이와 같은 요구사항이 제가 하고 있는 고민을 유도하고 있는 것 같네요 😅)

🙋🏻‍♂️ 검증 책임에 대해 크루들과 토의하고 결론을 내려보았습니다

크루들끼리 토의하고 스스로 결론 내려보았습니다. 사고 과정과 도출한 결론이 합리적인지 썬께서 봐주시면 너무 좋을 것 같아요 😃

시작은 이렇습니다.
블랙잭 게임에 참여하는 플레이어는 3명 이하여야 한다와 같은 검증로직을 저는 Players 도메인에 위치시켰습니다.

그런데 문득 이런 생각이 들었어요

  • 플레이어가 3명이하이어야 하는 것을 검증하는 책임은 Players의 책임이 아니라 BlackJackGame의 책임이 아닐까
  • Players 도메인은 사실 자신이 관리하고 있는 플레이어들만 알고 있으면 되지 3명 이하여야 한다는 게임의 룰까지 알아야 될까

이런 생각 이후로 Players 도메인이 순수해보이지 않았습니다. (자신의 역할이외의 룰까지 알고 있어서 순수하지 않다라는 생각..)

간단히 넘어갈 수도 있는 문제였지만 저는 그때부터 다른 도메인들도 의심스러워지기 시작했어요 🫠

  • 21이라는 숫자는 Hand도메인이 알아도 되나? 핸드는 카드리스트만 관리하면 되는 것 아닌가?
  • 플레이어는 자신이 히트할 수 있는 숫자를 알아도 되나? 룰과 너무 강한 결합을 맺는 것 아닌가?
  • 도메인을 여러 룰들, 전략에서 멀리 하여 순수하게 두는 것이 재사용성에 더 좋지 않을까?

이러한 고민을 크루들과 얘기해보았고 우선 다음과 같이 스스로 결론을 내보았습니다.

  • 블랙잭 게임 프로젝트에 존재하는 도메인들은 블랙잭 게임 구현이라는 같은 목표를 가진다.
  • 같은 목표, 같은 요구사항을 바라보고 있기에 각각의 도메인들에게서 규칙을 엄격하게 덜어낼 필요 없다.
  • 같은 목표를 위한 각자의 요구사항을 알고 있는 것은 합리적.
  • 분산된 책임을 바탕으로 하나의 목표를 이행하도록 협력하게 하는 것이 중요
  • 그리고 룰을 덜어내고 BlackJackGame에만 추가하면 오히려 BlackJackGame클래스가 뚱뚱해진다.
  • 현행처럼 각각의 도메인에 맞는 검증을 가지도록 하자

위 관점에서 보면 Players도메인은 BlackJackGame을 위한 Players로 볼 수 있을 것이고 이제는 Players가 3명이하와 같은 게임 규칙을 알아도 문제없다고 생각하게 되었습니다.

저의 결론인데 어떻게 생각하시나요 👀

🙋🏻‍♂️ 특정 기능의 책임 위치를 정하는 요령을 생각해보았습니다.

특정 기능을 어느 도메인의 책임으로 둘 지 스스로 판단하기 힘들었습니다.

1,2단계 모두 이 클래스의 책임에 이 기능은 합리적인듯,이 클래스에 위치하기에 위화감 없는듯과 같이 느낌에 의존한 설계를 많이했었어요.

특히 이 과정에서 자꾸 행위를 추상화하려고 하다가 객체지향의 특성을 많이 놓친 경험도 있습니다.
(ex. 점수 계산시 Player에게 Hand를 게터로 꺼내와서 ScoreCalculator에 전달)

크루 중에 승부를 내는 기능을 Dealer에 위치시킨 크루가 있었고 이 크루와 자세히 얘기해볼 수 있는 기회가 있었습니다.

리비: 나는 GameResult를 판단하는 기능은 Judge 혹은 Referee에 두어야 한다고 생각이 드는데 왜 dealer에다가 두었어? 딜러가 GameResult까지 판단하는 것은 책임분리가 잘 되지 않은 것 같아. 또 다른 개발자가 게임 결과 판정 관련 로직을 찾아올 때 딜러를 찾아오기는 힘들지 않을까?

???: 분리를 엄격히 지키지 못하더라도 우선 실제로 결과를 내기 위해서 상호작용하는 객체는 딜러와 플레이어라고 생각해. Referee나 Judge같은 경우는 도메인들끼리의 상호작용을 돕는 것이 아니라 행위를 서비스레이어처럼 추상화함으로 객체지향적인 설계를 챙기지 못한다는 생각이야. 상호작용하는 객체에게 직접 시킨다면 그게 객체지향적인 설계 아닐까?

크루들과 얘기를 한 다음에 저는 설득되어 다음과 같은 결론을 내리게 되었어요

  • TDA 원칙을 생각해보자.
  • 어떤 기능이 필요로 하는 속성을 가지는 클래스에 물어보도록 하자
  • 실제로 상호작용하는 도메인이 해당 책임을 가져야 하는 것은 아닌지 생각해보자
  • 도메인들끼리의 상호작용을 위해서 getter로 꺼내서 서비스스러운 클래스에 던지는 데이터 주도 개발은 지양하자
  • 철저한 분리는 오히려 파생되는 클래스를 많게 하고 복잡도를 올릴 수도 있다.

앞으로 위치를 정의할 때 필요한 속성을 가지고 있는 객체, 그리고 실제로 상호작용이 이루어지는 객체에게 물어보도록 할까 합니다.
썬이 보시기엔 어떤가요 🤔

@syoun602
Copy link

syoun602 commented Mar 17, 2024

  1. 도메인 및 연관관계
    객체지향 설계에 있어서 절대적으로 정답이 있지는 않은 것 같아요. 따라서 제 얘기가 정답이 아님을 우선 상기시켜드립니다.
    저 또한 플레이어에 대한 베팅이라면 충분히 플레이어가 가지고 있어도 합리적이라고 생각합니다. 다만, 이때 딜러는 베팅 금액을 필요로 하지 않기 때문에 상속이 아닌 조합을 활용했을 것 같구요. 이렇게하면 3개 이상의 인스턴스 변수에 대한 요구사항도 만족할 것 같네요 😄
    설계를 위해서 객체들을 떠올리기 이전에 어떤 메시지가 있을까?를 먼저 고민해보면 좋을 것 같아요. 예를 들어 베팅하기라는 메시지를 정의해보고 이 기능은 누구의 책임일까?를 생각해보는거죠. 그리고 이때 해당 객체가 스스로 처리할 수 없는 정보 또는 기능이 있다면 (win, lose에 따른 변화) 적절한 객체를 찾아 해당 작업을 위임해볼 수 있을 것 같아요. 이걸 책임주도설계(RDD)라고 부르는데 더 궁금하시다면 책 오브젝트를 추천드립니다 :)

  2. 검증 책임, 책임 위치
    객체지향에 한없이 빠지다보면 갑자기 주화입마가 오는데, 잘 정리하신 것 같습니다 👍
    결국 객체지향도 하나의 어플리케이션을 완성시키기 위한 수단임으로 도메인/룰에 대한 책임을 각각에게 부여해도 문제가되지 않을 것 같아요.

Copy link

@syoun602 syoun602 left a comment

Choose a reason for hiding this comment

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

안녕하세요 리비 !

블랙잭 미션 잘 구현해주셨습니다 💯
이전에도 충분히 코드를 잘 작성해주셔서 이만 머지하겠습니다 :)
이제 레벨 1은 악명높은 체스 미션만 남았는데, 푹 쉬시고 다음 미션도 화이팅입니다 😊

@@ -3,12 +3,13 @@
import blackjack.domain.player.Dealer;
import blackjack.domain.player.Player;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.function.BiPredicate;

public enum GameResult {
Copy link

@syoun602 syoun602 Mar 17, 2024

Choose a reason for hiding this comment

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

넵 현재도 충분히 좋은 방법인 것 같아요 👍
다만, 이렇게 될 경우 enum의 순서가 로직에 영향을 미치고 있는데, 이를 인지 못한 누군가가 결과를 추가/변경할 때 실수할 여지가 있을 것 같아요. 모든 케이스를 추가해볼 수도 있겠네요 🙂
추가) 딜러 - (A, 10) 블랙잭, 플레이어 - (5, 6, 10) 단순 21 에 대한 로직이 누락되었습니다!

저라면 좀 더 변경에 유연한 아래와 같은 if문을 사용할 것 같긴 한데,
코드에 정답이 없고 개인의 판단에 따라 달라지는 부분이라고 생각합니다~!

public Result judge(Dealer dealer, Player player) {
    if (player.isBusted() || dealer.isBusted()) {
        judgeBust()
    }
    
    if (player.isBlackjack() || dealer.isBlackjack()) {
        judgeBlackjack()
    }
    
    return judgeByScore()
}

.findFirst()
.orElseThrow(() -> new IllegalArgumentException("[INTERNAL ERROR] 게임 결과를 판정할 수 없습니다"));
.findAny()
.orElseThrow(() -> new NoSuchElementException("[INTERNAL ERROR] 게임 결과를 판정할 수 없습니다"));

Choose a reason for hiding this comment

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

NoSuchElementException을 의도한건 맞습니다!
자주 사용하는 표준 예외에 대해서는 이펙티브 자바 아이템 72. 표준예외를 사용하라를 참고해보면 좋을 것 같아요.
근데 저도 항상 기억하고 있지는 않아서 한번씩 다시 찾아보는 편입니다 🤣

@@ -21,11 +22,11 @@ public enum GameResult {
this.biPredicate = biPredicate;
}

public static GameResult of(Dealer dealer, Player player) {
public static GameResult judge(Dealer dealer, Player player) {

Choose a reason for hiding this comment

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

👍

this.cards = cards;
}

CardDeck(List<Card> cards) {
this((Queue<Card>) new LinkedList<>(cards));

Choose a reason for hiding this comment

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

Suggested change
this((Queue<Card>) new LinkedList<>(cards));
this(new ArrayDeque<>(cards));

요렇게 해볼 수 있겠네요 🙂
강제 타입 변환은 추천드리지 않습니다

@syoun602 syoun602 merged commit 36007cf into woowacourse:libienz Mar 17, 2024
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.

2 participants