-
Notifications
You must be signed in to change notification settings - Fork 355
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
base: 2chang5
Are you sure you want to change the base?
[Step2] 로또 제출합니다. #1093
Changes from all commits
6eb0173
5c99694
b3d48c8
ae096bb
9f2279f
ea37112
cb2c683
e1c9dc5
9d52853
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
||
resultView.showPurchaseCount(purchaseCount) | ||
val lottoBunch = LottoBunch(List(purchaseCount) { Lotto(RandomGenerator) }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수업 시간에도 다뤄졌던 것처럼 부생성자를 활용을 고려해볼 수 있겠네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 책 엘레강트 오브젝트의 인자의 값으로 NULL을 절대 허용하지 마세요 파트를 참고해보시면 좋을 것 같아요. 더불어 현재의 init-require 로직은 어떤 것을 검증하고자 하는지 직관적으로 이해하기 어렵습니다. 다른 개발자들은 해당 로직을 보고 어떻게 이해할지 고민해보면 좋겠습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutable 타입 없이도 원하는 바를 구현하실 수 있습니다 🙂 Kotlin의 Collections API를 활용해보세요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 힌트 또한 참고해보시면 어떨까요?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (1..45).shuffled().take(6).toSet() 힌트를 참고한다면 이런식으로 구현할 수 있을거것 같지만 |
||
} | ||
|
||
private fun getByManual(lottoNumber: IntArray): Set<LottoNumber> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. var 없이도 구현할 수 있습니다. |
||
} | ||
return totalPrize.toDouble() / purchaseAmount | ||
} | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LottoNumber를 새로 만들 때마다 Range 객체가 다시 만들어지고 있어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 range가 객체생성이라는 감각조차 없었네요 좋은 리뷰 감사합니다. |
||
} | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일종의 지연 초기화를 구현해주신 것 같은데요, |
||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 테스트 가능성을 높이기 위한 인터페이스를 잘 설계해주셨지만, 막상 테스트에서는 |
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] | ||
} | ||
} |
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() | ||
} |
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() } | ||
} | ||
} |
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)}입니다.") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# 로또 | ||
로또를 만드는게 아니라 로또 당첨됐으면 좋겠다. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도요. |
||
|
||
## 도메인 | ||
- [x] 로또를 생성한다. | ||
- [x] 로또 번호를 랜덤하게 생성한다. | ||
- [x] 로또 번호는 중복될 수 없다. | ||
- [x] 당첨 로또 수를 계산한다. | ||
- [x] 몇등 당첨인지 확인한다. | ||
- [x] 수익률을 계산한다. | ||
- [x] 로또 번호는 1~45사이로 한정한다. | ||
|
||
|
||
## 뷰 | ||
### 입력 | ||
- [ ] 구입금액을 입력받는다. | ||
- [ ] 로또 당첨 번호를 입력받는다. | ||
### 출력 | ||
- [ ] 구매 내역을 출력해준다.(구매 갯수,로또 내역) | ||
- [ ] 결과를 출력해준다. |
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()) }) | ||
} | ||
} |
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) } | ||
} | ||
} | ||
}) |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 로직이 컨트롤러에 있는 것이 적절할까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
도메인의 책임이라 생각하여 PurchaseAmount 객체를 신설하여 내부로 로직을 밀어넣었습니다.