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

[Step3] 로또(2등) 구현 #1084

Merged
merged 9 commits into from
Nov 28, 2024
28 changes: 28 additions & 0 deletions src/main/kotlin/lotto/LottoCreator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lotto

import lotto.const.LottoConst
import lotto.domain.Lotto
import lotto.domain.LottoNumber
import lotto.domain.WinningLotto
import lotto.util.NumberGenerator
import lotto.util.RandomNumberGenerator

class LottoCreator(private val numberGenerator: NumberGenerator = RandomNumberGenerator()) {
fun createLottos(count: Int): List<Lotto> {
return List(count) { createSingleLotto() }
}

fun createWinningLotto(
winningNumbers: Set<Int>,
bonusNumber: Int,
): WinningLotto {
val winningLotto = Lotto(winningNumbers.map { LottoNumber(it) }.toSet())
return WinningLotto(winningLotto, LottoConst.getLottoNumber(bonusNumber))
}

private fun createSingleLotto(): Lotto {
val randomNumbers = numberGenerator.generate()
val lotto = Lotto(randomNumbers.map { LottoConst.LOTTO_NUMBERS[it - 1] }.toSet())
Comment on lines +24 to +25

Choose a reason for hiding this comment

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

미리생성한 로또넘버풀을 활용하면 어떨까요?
shuffled, take 등의 키워드를 활용해보아요!

return lotto
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/lotto/LottoShop.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lotto

import lotto.const.LottoConst.UNIT_OF_AMOUNT
import lotto.domain.Order

class LottoShop(private val lottoCreator: LottoCreator) {
fun makeOrder(amount: Int): Order {
validateAmountIsPositive(amount)
val lottoCounts = calculateLottoCounts(amount)
return Order(amount, lottoCreator.createLottos(lottoCounts))
}

private fun validateAmountIsPositive(amount: Int) {
require(amount > 0) { "로또 구매 금액은 음수이거나 0원일 수 없습니다. (현재 입력 금액: $amount)" }
}

private fun calculateLottoCounts(amount: Int): Int {
require(amount % UNIT_OF_AMOUNT == 0) { "로또 구매 금액은 1000원 단위로 입력되어야 합니다. (현재 입력 금액: $amount)" }
return amount / UNIT_OF_AMOUNT
}
}
18 changes: 0 additions & 18 deletions src/main/kotlin/lotto/LottoSystem.kt

This file was deleted.

17 changes: 13 additions & 4 deletions src/main/kotlin/lotto/Main.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package lotto

import lotto.view.InputView
import lotto.view.ResultView

fun main() {
val lottoSystem = LottoSystem()
val lottoCreator = LottoCreator()
val lottoShop = LottoShop(lottoCreator)
val winningLottoMatcher = WinningLottoMatcher()

// 주문 생성
val amount = InputView.getAmount()
val order = lottoSystem.createOrder(amount)
val order = lottoShop.makeOrder(amount)
ResultView.printCreatedLottos(order.lottos)

// 당첨번호 및 보너스번호 생성
val winNumberInput = InputView.getWinNumberInput()
val winNumbers = lottoSystem.createWinNumbers(winNumberInput)
val bonusNumber = InputView.getBonusNumber()
val winNumbers = lottoCreator.createWinningLotto(winNumberInput, bonusNumber)

val result = lottoSystem.createWinningResult(order, winNumbers)
// 결과 출력
val result = winningLottoMatcher.checkAndGetResult(order, winNumbers)
ResultView.printResult(result)
}
27 changes: 0 additions & 27 deletions src/main/kotlin/lotto/Order.kt

This file was deleted.

18 changes: 0 additions & 18 deletions src/main/kotlin/lotto/Prize.kt

This file was deleted.

10 changes: 0 additions & 10 deletions src/main/kotlin/lotto/RankResult.kt

This file was deleted.

18 changes: 0 additions & 18 deletions src/main/kotlin/lotto/ResultView.kt

This file was deleted.

41 changes: 41 additions & 0 deletions src/main/kotlin/lotto/WinningLottoMatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto

import lotto.domain.Lotto
import lotto.domain.LottoResult
import lotto.domain.Order
import lotto.domain.Rank
import lotto.domain.WinningLotto
import lotto.domain.WinningResult

class WinningLottoMatcher {

Choose a reason for hiding this comment

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

WinningLottoMatcher의 책임은 무엇일까요?

WinningLotto를 파라미터로 받아서 WinningLotto의 함수를 실행시켜주고 있네요
WinningLotto의 책임은 아닐까요?

추가로 결과를 확인하기보단 결과를 가공해주는 로직이 더 많은건 아닐까요 :)
추첨 결과를 받아서 가공하는 책임은 따로 분리해도 좋을거같단생각이 들어요!
이부분 추가로 고민해보면 좋을거 같아요

fun checkAndGetResult(
order: Order,
winningLotto: WinningLotto,
): WinningResult {
val winningMatchCounts = aggregateLottoResult(order.lottos, winningLotto)
return WinningResult(winningMatchCounts, order.amount)
}

private fun aggregateLottoResult(
lottos: List<Lotto>,
winningLotto: WinningLotto,
): List<LottoResult> {
val result = groupByRank(lottos, winningLotto)
return result
.map { (rank, count) -> LottoResult(count, rank) }
.sortedBy { it.rank.prizeAmount }
}

private fun groupByRank(
lottos: List<Lotto>,
winningLotto: WinningLotto,
): Map<Rank, Int> {
val result = Rank.entries.filter { it !== Rank.MISS }.associateWith { 0 }
val rankWithMatchCounts =
lottos.map { winningLotto.matchLotto(it) }
.filter { it !== Rank.MISS }
.groupBy { it }
.mapValues { (_, values) -> values.size }
return result.mapValues { (rank, count) -> rankWithMatchCounts[rank] ?: count }
}
}
49 changes: 0 additions & 49 deletions src/main/kotlin/lotto/WinningResult.kt

This file was deleted.

12 changes: 12 additions & 0 deletions src/main/kotlin/lotto/const/LottoConst.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package lotto.const

import lotto.domain.LottoNumber

object LottoConst {

Choose a reason for hiding this comment

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

현재 공통으로 사용되는 상수값들을 LottoConst라는 object를 만들어서 모아두고 사용중입니다. 남재님은 상수값들이 여러곳에서 필요할때 어떻게 처리하시는지 궁금합니다!

저는 개인적으로 Const같은 상수 집합을 만드는것을 선호하지는 않아요!
프로젝트가 커지다보면, 상수가 많아져서 관리가 힘들기도 하고
관리의 범위가 애매해지는것 같아요 :)

LottoConst라는 상수집합을 만들기보단, 상수값은 보통 책임있는곳에서 정의하는 편 입니다!
UNIT_OF_AMOUNT로 선언하기 보단 Lotto.Price LottoShop.Price 와 같이 좀더 상수가 명확해지는 효과도 노릴수 있어요!
추가적으로 LOTTO_NUMBERS 관련된 기능은 Creator 객체의 책임은 아닐까요?

const val UNIT_OF_AMOUNT = 1000
val LOTTO_NUMBERS = IntRange(1, 45).map { LottoNumber(it) }.toList()

fun getLottoNumber(number: Int): LottoNumber {
return requireNotNull(LOTTO_NUMBERS.find { it == LottoNumber(number) }) { "일치하는 번호의 로또번호가 존재하지 않습니다." }
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
package lotto

class Lotto(generatedNumbers: Set<Int>) {
val numbers: Set<LottoNumber>
package lotto.domain

class Lotto(val numbers: Set<LottoNumber>) {
init {
validateSize(generatedNumbers)
numbers = generatedNumbers.map { LottoNumber(it) }.toSet()
}

fun countMatchingNumbers(targetLotto: Set<LottoNumber>): Int {
return targetLotto.count { it in this.numbers }
validateSize(numbers)
}

private fun validateSize(numbers: Set<Int>) {
private fun validateSize(numbers: Set<LottoNumber>) {
require(numbers.size == LOTTO_NUMBER_SIZE) { "로또 번호는 ${LOTTO_NUMBER_SIZE}개여야 합니다. 현재 전달된 개수는 ${numbers.size}개 입니다." }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lotto
package lotto.domain

@JvmInline
value class LottoNumber(private val number: Int) {
Expand All @@ -10,6 +10,10 @@ value class LottoNumber(private val number: Int) {
require(this.number in LOTTO_RANGE) { "로또 번호는 ${LOTTO_RANGE.first} ~ ${LOTTO_RANGE.last} 내의 숫자여야 합니다." }
}

override fun toString(): String {
return "$number"
}

companion object {
private val LOTTO_RANGE = 1..45
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/lotto/domain/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package lotto.domain

data class LottoResult(
val totalCount: Int,
val rank: Rank,
) {
Comment on lines +3 to +6

Choose a reason for hiding this comment

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

로또의 결과보다는 로또의 통계를 나타내는건 아닐까요 ?:)

fun getTotalPrizeMoney(): Int {
return this.rank.prizeAmount * totalCount
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/domain/Order.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.domain

import lotto.const.LottoConst.UNIT_OF_AMOUNT

data class Order(
val amount: Int,
val lottos: List<Lotto>,
) {
init {
validateLottoCounts()
}

private fun validateLottoCounts() {
val count = amount / UNIT_OF_AMOUNT
require(lottos.size == count) { "구매한 금액과 로또의 수량이 일치하지 않습니다." }
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/lotto/domain/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package lotto.domain

enum class Rank(
val matchCount: Int,
val prizeAmount: Int,
private val match: (Int, Boolean) -> Boolean,
) {
FIRST(6, 2_000_000_000, { count, _ -> count == 6 }),
SECOND(5, 30_000_000, { count, isBonusMatch -> count == 5 && isBonusMatch }),
THIRD(5, 1_500_000, { count, _ -> count == 5 }),
FOURTH(4, 50_000, { count, _ -> count == 4 }),
FIFTH(3, 5_000, { count, _ -> count == 3 }),
MISS(0, 0, { count, _ -> count < 3 }), ;

companion object {
fun findByMatchCount(
matchCount: Int,
isBonusMatch: Boolean = false,
): Rank {
return entries.firstOrNull { it.match(matchCount, isBonusMatch) }
?: throw RuntimeException("일치하는 숫자의 개수에 해당하는 상품이 존재하지 않습니다.")
Comment on lines +20 to +21

Choose a reason for hiding this comment

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

에러를 던지는것보다 null 이나 MISS 를 리턴함으로써
에러를 대체할수 있어요 :)
실제로 코틀린에서는 에러를 던지기보단 null을 반환하기도 해요!

Copy link
Author

@Seokho-Ham Seokho-Ham Nov 27, 2024

Choose a reason for hiding this comment

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

음 이건 궁금증이 생기는 부분이네요!
null을 반환하면 해당 결과값을 받는 코드에서는 nullable하게 처리가 될거 같습니다.
그럼 접근할때 NullPointException이 발생하는 경우가 발생할수도 있을텐데 예외를 던지는것보다 null로 던지는게 권장되는걸까요?! 실전에서는 이런 부분에 대해서는 보통 어떻게 처리를 하나요?

추가적으로 이번 과정을 하면서 예외를 명시적으로 던지는것을 권장하지 않는듯한 느낌을 받았는데 혹시 이유가 있을까요?!
(코틀린을 처음 사용해봐서 아직 모르는게 많아서 질문드립니다ㅎㅎ)

Choose a reason for hiding this comment

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

코틀린에서는 Checked Exception를 강제하지 않는 등의 이유로
예외를 명시적으로 던지는걸 권장하지는 않아요
경우에 따라 Null이나 sealed 클래스(ex> Result)를 많이 사용하곤 해요,

코틀린에서는 nullable인 경우에도 ?. , ?: 같은 키워드를 사용해서 nullSafe한 방법을 많이 활용하긴합니다!

}
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/lotto/domain/WinningLotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lotto.domain

class WinningLotto(
val winningNumbers: Lotto,
val bonusNumber: LottoNumber,
) {
Comment on lines +3 to +6

Choose a reason for hiding this comment

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

bonusNumber와 winningNumbers 중첩되는 상황도 고려하면 좋을거같아요 :)

fun matchLotto(targetLotto: Lotto): Rank {
return Rank.findByMatchCount(
countMatchingNumbers(targetLotto),
matchBonusNumber(targetLotto),
)
}

fun countMatchingNumbers(targetLotto: Lotto): Int {
return targetLotto.numbers.count { it in winningNumbers.numbers }
}

fun matchBonusNumber(targetLotto: Lotto): Boolean {
return targetLotto.numbers.contains(bonusNumber)
}
Comment on lines +14 to +20

Choose a reason for hiding this comment

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

외부에서 사용되지 않는 함수는 private으로 하면 어떨까요 :)

}
Loading