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

[Step2] 로또 제출합니다. #1093

Open
wants to merge 9 commits into
base: 2chang5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/main/kotlin/lotto/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lotto

import lotto.domain.Lotto
import lotto.domain.Lotto.Companion.LOTTO_PRICE
import lotto.domain.LottoBunch
import lotto.domain.LottoNumber
import lotto.domain.RandomGenerator
import lotto.view.InputView
import lotto.view.ResultView

fun main() {
val inputView = InputView()
val resultView = ResultView()

val purchaseAmount = inputView.getPurchaseAmount() ?: return
val purchaseCount = purchaseAmount / LOTTO_PRICE
Copy link
Member

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.

도메인의 책임이라 생각하여 PurchaseAmount 객체를 신설하여 내부로 로직을 밀어넣었습니다.


resultView.showPurchaseCount(purchaseCount)
val lottoBunch = LottoBunch(List(purchaseCount) { Lotto(RandomGenerator) })
Copy link
Member

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.

훨씬 깔끔해지고 좋은것 같습니다.
부생성자를 평소에 활용을 잘안하게 되었었는데 좋은 리뷰 감사드립니다.

resultView.showPurchaseLotto(lottoBunch)

val winningNumbers = inputView.getWinningNumbers() ?: return
val winningLottoNumbers = winningNumbers.map { LottoNumber.get(it) }

resultView.showResultInterface()
resultView.showMatchLottoResult(lottoBunch.getMatchLottoResult(winningLottoNumbers))
resultView.showYield(lottoBunch.getYield(winningLottoNumbers, purchaseAmount))
}
41 changes: 41 additions & 0 deletions src/main/kotlin/lotto/domain/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto.domain

class Lotto(private val lottoNumberGenerator: LottoNumberGenerator? = null, vararg lottoNumber: Int) {
Copy link
Member

Choose a reason for hiding this comment

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

책 엘레강트 오브젝트의 인자의 값으로 NULL을 절대 허용하지 마세요 파트를 참고해보시면 좋을 것 같아요.

더불어 현재의 init-require 로직은 어떤 것을 검증하고자 하는지 직관적으로 이해하기 어렵습니다. 다른 개발자들은 해당 로직을 보고 어떻게 이해할지 고민해보면 좋겠습니다.

Copy link
Member

Choose a reason for hiding this comment

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

만약 lottoNumber가 10자리 들어오게 되면 어떨까요? 🤔

init {
require(!(lottoNumberGenerator == null && lottoNumber.isEmpty())) { LOTTO_PARAMETERS_ERROR_MESSAGE }
}

val lottoNumbers: Set<LottoNumber> =
if (lottoNumber.isNotEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

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

다른 개발자들도 로또의 자동/수동 여부를 lottoNumber가 비어있는지 여부로 판단한다고 이해할 수 있을까요?

getByManual(lottoNumber)
} else {
getByAuto()
}

private fun getByAuto(): MutableSet<LottoNumber> {
lottoNumberGenerator ?: return mutableSetOf()
val lotto: MutableSet<LottoNumber> = mutableSetOf()
while (lotto.size < LOTTO_NUMBER_COUNT) {
lotto.add(LottoNumber.get(lottoNumberGenerator.generateLottoNumber()))
}
return lotto
Comment on lines +16 to +21
Copy link
Member

Choose a reason for hiding this comment

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

Mutable 타입 없이도 원하는 바를 구현하실 수 있습니다 🙂 Kotlin의 Collections API를 활용해보세요.

Copy link
Member

Choose a reason for hiding this comment

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

힌트 또한 참고해보시면 어떨까요?

로또 자동 생성은 shuffled()을 활용한다.
sorted()를 활용해 정렬 가능하다.
contains()를 활용하면 어떤 값이 존재하는지 유무를 판단할 수 있다.

Copy link
Author

Choose a reason for hiding this comment

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

(1..45).shuffled().take(6).toSet()

힌트를 참고한다면 이런식으로 구현할 수 있을거것 같지만
buildSet이라는 api가 깔끔해보여서 해당 api를 사용해서 변경해보았습니다.

}

private fun getByManual(lottoNumber: IntArray): Set<LottoNumber> {
Copy link
Member

Choose a reason for hiding this comment

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

IntArray를 사용하신 이유가 있으실까요?

require(lottoNumber.size == LOTTO_NUMBER_COUNT) { LOTTO_NUMBER_COUNT_EXCEPTION_MESSAGE }
require(lottoNumber.distinct().size == LOTTO_NUMBER_COUNT) { LOTTO_NUMBER_DISTINCT_MESSAGE }
Copy link
Member

Choose a reason for hiding this comment

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

Set 자료형을 활용하면서 distinct() 유효성 검사를 또 할 필요가 있을까요?

return lottoNumber.map { LottoNumber.get(it) }.toSet()
}

fun match(winningNumber: List<LottoNumber>): MatchingResult? =
MatchingResult.fromMatchNumber(lottoNumbers.intersect(winningNumber).size)

companion object {
private const val LOTTO_NUMBER_COUNT = 6
private const val LOTTO_NUMBER_COUNT_EXCEPTION_MESSAGE = "로또 초기화시 입력된 로또 번호가 6개가 아닙니다."
private const val LOTTO_NUMBER_DISTINCT_MESSAGE = "로또 초기화시 입력된 로또 번호에 중복이 있습니다."
private const val LOTTO_PARAMETERS_ERROR_MESSAGE = "로또 생성시 필요한 매개변수를 잘못입력하셨습니다."

const val LOTTO_PRICE = 1000
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/lotto/domain/LottoBunch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package lotto.domain

class LottoBunch(val value: List<Lotto>) {
fun getMatchLottoResult(winningNumbers: List<LottoNumber>): Map<MatchingResult, Int> {
val result: MutableMap<MatchingResult, Int> = MatchingResult.entries.associateWith { 0 }.toMutableMap()
Copy link
Member

Choose a reason for hiding this comment

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

MatchingResult의 값을 모두 꺼내어 외부에서 판단하기보다, MatchingResult에게 일부 역할과 책임을 부여하면 어떨까요?

value.forEach {
val stepResult = it.match(winningNumbers) ?: return@forEach
result[stepResult] = result.getOrDefault(stepResult, 0) + 1
}
return result
}
Comment on lines +4 to +11
Copy link
Member

Choose a reason for hiding this comment

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

이 로직 또한 Mutable 타입 없이도 구현할 수 있습니다.


fun getYield(
winningNumbers: List<LottoNumber>,
purchaseAmount: Int,
): Double {
val matchResults: Map<MatchingResult, Int> = getMatchLottoResult(winningNumbers)
var totalPrize = 0
matchResults.forEach { (matchingResult, winCount) ->
totalPrize += (matchingResult.prizeAmount * winCount)
Copy link
Member

Choose a reason for hiding this comment

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

var 없이도 구현할 수 있습니다.

}
return totalPrize.toDouble() / purchaseAmount
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/domain/LottoNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.domain

@JvmInline
value class LottoNumber private constructor(val value: Int) {
init {
require(value in 1..45)
Copy link
Member

Choose a reason for hiding this comment

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

LottoNumber를 새로 만들 때마다 Range 객체가 다시 만들어지고 있어요.
어떻게 개선할 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

오 range가 객체생성이라는 감각조차 없었네요 좋은 리뷰 감사합니다.
1회만 생성하도록 변경하였습니다.

}

companion object {
private val lottoNumbers: MutableMap<Int, LottoNumber> = mutableMapOf()

fun get(lottoNumber: Int): LottoNumber =
lottoNumbers[lottoNumber] ?: LottoNumber(lottoNumber).apply {
lottoNumbers[lottoNumber] = this
}
Comment on lines +10 to +15
Copy link
Member

Choose a reason for hiding this comment

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

일종의 지연 초기화를 구현해주신 것 같은데요,
사실 로또 프로그램처럼 실제로 발생할 수 있는 로또 숫자가 제한적인 경우 최초에 참조될 때 1~45까지의 숫자를 모두 초기화해도 괜찮다고 생각하긴 합니다. 이 경우 굳이 MutableMap을 활용하지 않아도 되어요. 주관적인 의견이니 참고만 해주셔도 좋아요!

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

interface LottoNumberGenerator {
fun generateLottoNumber(): Int
}
Comment on lines +3 to +5
Copy link
Member

Choose a reason for hiding this comment

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

테스트 가능성을 높이기 위한 인터페이스를 잘 설계해주셨지만, 막상 테스트에서는 LottoNumberGenerator의 구현체를 따로 만들어 활용하지 않고 있네요!?

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

enum class MatchingResult(val prizeAmount: Int, val matchNumber: Int) {
MATCHED_THREE(5_000, 3),
MATCHED_FOUR(50_000, 4),
MATCHED_FIVE(1_500_000, 5),
MATCHED_SIX(2_000_000_000, 6), ;

companion object {
private const val MATCH_NUMBER_TRANSFER_ERROR_MESSAGE = "로또 결과 이넘값 변환 오류가 발생하였습니다."
private val matchNumberToMatchResultMap = entries.associateBy { it.matchNumber }

fun fromMatchNumber(matchNumber: Int): MatchingResult? = matchNumberToMatchResultMap[matchNumber]
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/lotto/domain/RandomGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package lotto.domain

object RandomGenerator : LottoNumberGenerator {
override fun generateLottoNumber(): Int = (1..45).random()
}
34 changes: 34 additions & 0 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lotto.view

class InputView {
fun getPurchaseAmount(): Int? {
println("구입금액을 입력해 주세요.")
val input = readlnOrNull()
if (input.isNullOrEmpty()) {
println("아무값도 입력되지 않았습니다.")
return null
}
if (input.toIntOrNull() == null) {
println("숫자만 입력 가능합니다.")
return null
}
return input.toInt()
}

fun getWinningNumbers(): List<Int>? {
println("지난 주 당첨 번호를 입력해 주세요.")
val input = readlnOrNull()
if (input.isNullOrEmpty()) {
println("아무값도 입력되지 않았습니다.")
return null
}
val slicedInput = input.split(",")
slicedInput.forEach {
if (it.toIntOrNull() == null) {
println("숫자만 입력 가능합니다.")
return null
}
}
return slicedInput.map { it.toInt() }
}
}
31 changes: 31 additions & 0 deletions src/main/kotlin/lotto/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package lotto.view

import lotto.domain.LottoBunch
import lotto.domain.MatchingResult

class ResultView {
fun showPurchaseCount(purchaseCount: Int) {
println("${purchaseCount}개를 구매했습니다.")
}

fun showPurchaseLotto(lottoBunch: LottoBunch) {
lottoBunch.value.forEach {
println(it.lottoNumbers.map { it.value }.toString())
}
}

fun showResultInterface() {
println("당첨 통계")
println("----------")
}

fun showMatchLottoResult(result: Map<MatchingResult, Int>) {
result.forEach { key, value ->
println("${key.matchNumber}개 일치 (${key.prizeAmount}원)- ${value}개")
}
}

fun showYield(yield: Double) {
println("총 수익률은 ${String.format("%.2f", yield)}입니다.")
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/lotto/기능목록.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 로또
로또를 만드는게 아니라 로또 당첨됐으면 좋겠다.
Copy link
Member

Choose a reason for hiding this comment

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

저도요.
제발


## 도메인
- [x] 로또를 생성한다.
- [x] 로또 번호를 랜덤하게 생성한다.
- [x] 로또 번호는 중복될 수 없다.
- [x] 당첨 로또 수를 계산한다.
- [x] 몇등 당첨인지 확인한다.
- [x] 수익률을 계산한다.
- [x] 로또 번호는 1~45사이로 한정한다.


## 뷰
### 입력
- [ ] 구입금액을 입력받는다.
- [ ] 로또 당첨 번호를 입력받는다.
### 출력
- [ ] 구매 내역을 출력해준다.(구매 갯수,로또 내역)
- [ ] 결과를 출력해준다.
54 changes: 54 additions & 0 deletions src/test/kotlin/lotto/domain/LottoBunchTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package lotto.domain

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe

class LottoBunchTest : StringSpec({
"당첨 로또수를 계산한다." {
val classUnderTest: LottoBunch =
getLottoBunch(
listOf(
listOf(1, 2, 3, 4, 5, 6),
listOf(1, 2, 3, 4, 5, 6),
listOf(1, 2, 3, 4, 5, 6),
listOf(1, 2, 3, 4, 5, 6),
listOf(1, 2, 3, 4, 5, 45),
listOf(1, 2, 3, 4, 5, 45),
listOf(1, 2, 3, 4, 5, 45),
listOf(1, 2, 3, 4, 44, 45),
listOf(1, 2, 3, 4, 44, 45),
listOf(1, 2, 3, 43, 44, 45),
),
)
val winningNumbers = listOf(1, 2, 3, 4, 5, 6).map { LottoNumber.get(it) }
val expected =
mapOf(
MatchingResult.MATCHED_THREE to 1,
MatchingResult.MATCHED_FOUR to 2,
MatchingResult.MATCHED_FIVE to 3,
MatchingResult.MATCHED_SIX to 4,
)
classUnderTest.getMatchLottoResult(winningNumbers) shouldBe expected
}

"수익률을 계산한다." {
val classUnderTest: LottoBunch =
getLottoBunch(
listOf(
listOf(1, 2, 3, 4, 5, 6),
listOf(1, 2, 3, 4, 5, 45),
listOf(1, 2, 3, 4, 44, 45),
listOf(1, 2, 3, 43, 44, 45),
),
)
val winningNumbers = listOf(1, 2, 3, 4, 5, 6).map { LottoNumber.get(it) }
val purchaseAmount = 10000
val totalPrize = MatchingResult.entries.map { it.prizeAmount }.sum()

classUnderTest.getYield(winningNumbers, purchaseAmount) shouldBe totalPrize.toDouble() / purchaseAmount
}
}) {
private companion object {
fun getLottoBunch(numbers: List<List<Int>>): LottoBunch = LottoBunch(numbers.map { Lotto(null, *it.toIntArray()) })
}
}
23 changes: 23 additions & 0 deletions src/test/kotlin/lotto/domain/LottoNumberTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package lotto.domain

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.core.spec.style.StringSpec
import io.kotest.inspectors.forAll

class LottoNumberTest : StringSpec({
"로또 번호는 1~45 사이 값이여야 한다." {
(1..45).toList().forAll { lottoNumber ->
shouldNotThrowAny { LottoNumber.get(lottoNumber) }
}
}

"1~45이 아닌 값으로 로또 번호를 생성할시 에러가 발생한다." {
(-45..0).toList().forAll { lottoNumber ->
shouldThrowAny { LottoNumber.get(lottoNumber) }
}
(46..100).toList().forAll { lottoNumber ->
shouldThrowAny { LottoNumber.get(lottoNumber) }
}
}
})
41 changes: 41 additions & 0 deletions src/test/kotlin/lotto/domain/LottoTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto.domain

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.core.spec.style.StringSpec
import io.kotest.inspectors.forAll
import io.kotest.matchers.shouldBe

class LottoTest : StringSpec({
"로또를 발급한다." {
shouldNotThrowAny {
Lotto(RandomGenerator)
}
}

"로또를 직접 초기화할때 중복되지 않은 6개의 숫자를 입력해야한다." {
listOf(listOf(1, 2, 3, 4), listOf(1, 1, 2, 3, 4, 5)).forAll { numberList ->
shouldThrowAny {
Lotto(null, *numberList.toIntArray())
}
}
}

"발급된 로또의 로또번호 갯수는 6개이다." {
Lotto(RandomGenerator).lottoNumbers.size shouldBe 6
Copy link
Member

Choose a reason for hiding this comment

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

랜덤 로직을 참조하고 있는 테스트는 매번 실행할때마다 어떤 결과가 나올지 보장할 수 없습니다.
실제 객체에 버그가 있다고 하더라도 운이 좋게 통과할수도, 운이 나쁘게 실패할 수도 있다는 의미입니다.

}

"각 로또의 번호를 매칭하여 결과를 도출한다." {
listOf(
Pair(listOf(45, 44, 43, 42, 41, 40), null),
Pair(listOf(1, 45, 44, 43, 42, 41), null),
Pair(listOf(1, 2, 45, 44, 43, 42), null),
Pair(listOf(1, 2, 3, 45, 44, 43), MatchingResult.MATCHED_THREE),
Pair(listOf(1, 2, 3, 4, 45, 44), MatchingResult.MATCHED_FOUR),
Pair(listOf(1, 2, 3, 4, 5, 45), MatchingResult.MATCHED_FIVE),
Pair(listOf(1, 2, 3, 4, 5, 6), MatchingResult.MATCHED_SIX),
).forAll { (winningNumbers, matchingResult) ->
Lotto(null, 1, 2, 3, 4, 5, 6).match(winningNumbers.map { LottoNumber.get(it) }) shouldBe matchingResult
}
}
})