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

Step4 - 로또(수동) ver2 #985

Open
wants to merge 12 commits into
base: oyj7677
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
당첨 로또
- [O] 당첨 번호 일치 개수 반환
- [O] 보너스 번호 일치 검증
- [] 당첨번호 & 보너스 번호 중복
- [O] 당첨번호 & 보너스 번호 중복

로또 계산기
- [O] 수익률 반환
Expand All @@ -62,6 +62,6 @@ inputView (object)
- [O] 입력된 금액 포맷 검증
- [O] 숫자 포맷 검증
- [O] 금액 범위는 1,000원 이상 100,000원 이하의 정수
- [] 지난 당첨 번호 포맷 검증
- [O] 지난 당첨 번호 포맷 검증
- [O] 콤마(,)를 기준으로 나눈다
- [O] 숫자 포맷 검증
15 changes: 10 additions & 5 deletions src/main/kotlin/lotto/LottoMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ fun main() {

val cash = InputView.inputCash()
val lottoGame = LottoGame(RandomLogic())
val purchaseLottoList = lottoGame.buyLotto(cash.toInt())
val gameTimes = lottoGame.getGameTimes(cash)
val manualGameTimes = InputView.inputManualCnt(gameTimes)

OutputView.showLottoList(purchaseLottoList)
val numberCombinationList = InputView.inputNumberCombination(manualGameTimes)

val winningNumberList = InputView.inputWinningNumber()
val purchaseLottoList = lottoGame.buyLotto(gameTimes, numberCombinationList)
Copy link

Choose a reason for hiding this comment

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

좀 더 심화로 넘어간 키워드를 드리자면, 현재 Main은 컨트롤러와 UI가 섞여있다고 볼 수 있습니다.
그러다보니 설계를 하다보면 뭔가 섞이는 것 같다는 애매한 느낌을 받을 수 있습니다.
(저는 이 부분에서 많은 고민을 했었죠. )

웹서비스를 보면 프론트 영역에서 ajax로 요청해야하는 부분도 컨트롤러에 있는 것 같고, 응용SW로 보면 CLI의 영역을 컨트롤러가 같이 가지고 있는 것으로 보이죠.

그래서 이를 한번 더 분리해서 사용자와 입출력을 담당하는 cli계층과 말 그대로 하나의 요청에 대한 응답을 해주는 컨트롤러 영역을 분리해보는것도 좋은 경험이 될 것 같아요,.


OutputView.showLottoList(purchaseLottoList, manualGameTimes)

val winningNumberCombination = InputView.inputWinningNumber()
val bonusNumber = InputView.inputBonusNumber()

val winningLotto = LottoMachine.createWinningLotto(winningNumberList, bonusNumber)
val winningLotto = LottoMachine.createWinningLotto(winningNumberCombination, bonusNumber)
val winningStatus = lottoGame.getWinningStats(winningLotto, purchaseLottoList)
val winningRate = LottoMachine.createWinningRate(cash.toInt(), winningStatus)
val winningRate = LottoMachine.createWinningRate(cash, winningStatus)

OutputView.showWinningStatus(winningStatus, winningRate)
}
4 changes: 2 additions & 2 deletions src/main/kotlin/lotto/data/LottoNumber.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class LottoNumber private constructor(private val number: Int) : Comparable<Lott

private val NUMBERS: Map<Int, LottoNumber> = (MIN_NUMBER..MAX_NUMBER).associateWith(::LottoNumber)

fun createLottoNumbers(numbers: List<Int>): Set<LottoNumber> {
return LinkedHashSet(numbers.map(::from))
fun createLottoNumbers(numberCombination: NumberCombination): Set<LottoNumber> {
return LinkedHashSet(numberCombination.numberCombination.map(::from))
}

fun createRandomLottoNumber(lottoCreation: RandomLogicInterface): Set<LottoNumber> {
Expand Down
12 changes: 2 additions & 10 deletions src/main/kotlin/lotto/data/LottoRanking.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,10 @@ enum class LottoRanking(val matchingNumberCnt: Int, val price: Int) : Prize {

companion object {
fun findLottoRanking(matchingNumberCnt: Int, hasBonusNumber: Boolean): LottoRanking {
return if (matchingNumberCnt == SecondPlace.matchingNumberCnt) {
determineRankingWithBonusNumber(hasBonusNumber)
} else {
LottoRanking.values().find { it.matchingNumberCnt == matchingNumberCnt } ?: None
}
}

private fun determineRankingWithBonusNumber(isContainBonusNumber: Boolean): LottoRanking {
return if (isContainBonusNumber) {
return if (matchingNumberCnt == SecondPlace.matchingNumberCnt && hasBonusNumber) {
Copy link

Choose a reason for hiding this comment

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

이 부분이 많이 헷갈리시는것 같아요. 이쪽에서 제가 드렸던 피드백의 핵심은 상위 객체(LottoRanking)는 하위 객체가 무엇으로 결정되는지에 대해서 상세 조건을 알고싶어하지 않아야 한다는거죠.
지금은 그냥 매치카운트와 보너스번호 딱 두가지의 조건뿐이기에 한줄로도 정리가 되고 if-else로도 정리가 됩니다.
하지만 조건들이 수십개 이상이 된다면 이런 로직이 유지될 수 없겠죠.

반면, 상위객체에선 전달받은 객체만 하위 객체에 전달해서 맞는지에 대한 유무만 판단하도록한다면 로직이 추가되던 값이 바뀌던 뭐가 어떻게 되던 로직이 바뀔일이 없겠죠.

return values().find { it.support(matchingNumberCnt, hasBonusNumber) } ?: NONE

아 그리고 열거타입의 네이밍 컨벤션은 대문자 스네이크 케이스입니다 ㅎㅎ

SecondPlace
} else {
ThirdPlace
LottoRanking.values().find { it.matchingNumberCnt == matchingNumberCnt } ?: None
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/lotto/data/NumberCombination.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package lotto.data

class NumberCombination(val numberCombination: List<Int>)
Copy link

Choose a reason for hiding this comment

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

이건 오버 엔지니어링으로 보이네요 🤔
이 객체가 존재해야 할 이유가 명확하지 않은 것 같아요 ㅠㅠ

12 changes: 10 additions & 2 deletions src/main/kotlin/lotto/data/WinningLotto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ data class WinningLotto(val lotto: Lotto, val bonusNumber: LottoNumber) {
}

fun countMatchingNumbers(lotto: Lotto): Int {
return this.lotto.selectNumbers.intersect(lotto.selectNumbers).size
return this.lotto.matching(lotto)
}

fun hasBonusNumber(lotto: Lotto): Boolean {
Expand All @@ -19,6 +19,14 @@ data class WinningLotto(val lotto: Lotto, val bonusNumber: LottoNumber) {
}

private fun validateDuplicationBonusNumber() {
require(!lotto.selectNumbers.contains(bonusNumber)) { "당첨 번호 구성과 보너스 번호가 중복됩니다." }
require(!lotto.selectNumbers.contains(bonusNumber)) { ERR_MSG_DUPLICATION_BONUS_NUMBER }
Copy link

Choose a reason for hiding this comment

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

좀 더 객체 지향적으로 설계가 되었다면 다음과 같이 작성될 수 있을꺼에요.

Suggested change
require(!lotto.selectNumbers.contains(bonusNumber)) { ERR_MSG_DUPLICATION_BONUS_NUMBER }
require(!lotto.containNumber(bonusNumber)) { ERR_MSG_DUPLICATION_BONUS_NUMBER }

}

private fun Lotto.matching(lotto: Lotto): Int {
return this.selectNumbers.intersect(lotto.selectNumbers).size
}

companion object {
private const val ERR_MSG_DUPLICATION_BONUS_NUMBER = "당첨 번호 구성과 보너스 번호가 중복됩니다."
}
}
14 changes: 10 additions & 4 deletions src/main/kotlin/lotto/domain/LottoCalculator.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package lotto.domain

import lotto.data.LottoRanking
import java.util.EnumMap

object LottoCalculator {

private const val GAME_COST = 1000
private const val ERR_MSG_OVER_MANUAL_GAME_TIMES = "수동 게임 횟수가 총 게임 횟수를 초과하였습니다."

fun calculateWinningRate(cash: Int, winningStatus: Map<LottoRanking, Int>): Float {
fun calculateWinningRate(cash: Int, winningStatus: EnumMap<LottoRanking, Int>): Float {
val totalPrice = winningStatus.toList().sumOf { it.first.findPrize(it) }

return totalPrice / cash.toFloat()
}

fun getTimes(cash: Int): Int {
return cash / GAME_COST
fun getTimes(cash: Int, gameCost: Int): Int {
return cash / gameCost
}

fun getAutoTimes(totalTimes: Int, manualGameTimes: Int): Int {
require(totalTimes >= manualGameTimes) { ERR_MSG_OVER_MANUAL_GAME_TIMES }
return totalTimes - manualGameTimes
}
}
8 changes: 5 additions & 3 deletions src/main/kotlin/lotto/domain/LottoMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ package lotto.domain
import lotto.data.Lotto
import lotto.data.LottoNumber
import lotto.data.LottoRanking
import lotto.data.NumberCombination
import lotto.data.WinningLotto
import java.util.EnumMap

object LottoMachine {

fun createSelectLotto(lottoNumbers: Set<LottoNumber>): Lotto {
return Lotto(lottoNumbers)
}

fun createWinningLotto(winningNumbers: List<Int>, bonusNumber: Int): WinningLotto {
val winningLotto = LottoNumber.createLottoNumbers(winningNumbers)
fun createWinningLotto(winningNumberCombination: NumberCombination, bonusNumber: Int): WinningLotto {
val winningLotto = LottoNumber.createLottoNumbers(winningNumberCombination)
val bonusLottoNumber = LottoNumber.from(bonusNumber)
return WinningLotto(Lotto(winningLotto), bonusLottoNumber)
}
Expand All @@ -24,7 +26,7 @@ object LottoMachine {
return LottoRanking.findLottoRanking(matchingNumberCnt, hasBonusNumber)
}

fun createWinningRate(cash: Int, winningStatus: Map<LottoRanking, Int>): Float {
fun createWinningRate(cash: Int, winningStatus: EnumMap<LottoRanking, Int>): Float {
return LottoCalculator.calculateWinningRate(cash, winningStatus)
}
}
5 changes: 3 additions & 2 deletions src/main/kotlin/lotto/domain/WinningDomain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package lotto.domain
import lotto.data.Lotto
import lotto.data.LottoRanking
import lotto.data.WinningLotto
import java.util.EnumMap

object WinningDomain {

// 보너스 번호 추가.
fun checkWinningResult(winningLotto: WinningLotto, purchaseLottoList: List<Lotto>): Map<LottoRanking, Int> {
val winningStatusMap = mutableMapOf<LottoRanking, Int>()
fun checkWinningResult(winningLotto: WinningLotto, purchaseLottoList: List<Lotto>): EnumMap<LottoRanking, Int> {
val winningStatusMap: EnumMap<LottoRanking, Int> = EnumMap(LottoRanking::class.java)

purchaseLottoList.forEach {
val lottoRanking = LottoMachine.checkLotto(it, winningLotto)
Expand Down
30 changes: 24 additions & 6 deletions src/main/kotlin/lotto/service/LottoGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,32 @@ package lotto.service
import lotto.data.Lotto
import lotto.data.LottoNumber
import lotto.data.LottoRanking
import lotto.data.NumberCombination
import lotto.data.WinningLotto
import lotto.domain.LottoCalculator
import lotto.domain.LottoMachine
import lotto.domain.RandomLogicInterface
import lotto.domain.WinningDomain
import java.util.EnumMap

class LottoGame(private val randomLogic: RandomLogicInterface) {
class LottoGame(private val randomLogic: RandomLogicInterface, private val gameCost: Int = DEFAULT_COST) {
Copy link

Choose a reason for hiding this comment

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

private val gameCost: Int = DEFAULT_COST

해당 값은 stateful한 설계로 보이는데 서비스는 stateless를 지향해서 적절한 선언은 아닙니다 ㅎ


fun buyLotto(cash: Int): List<Lotto> {
val times = LottoCalculator.getTimes(cash)
fun getGameTimes(cash: Int): Int {
return LottoCalculator.getTimes(cash, gameCost)
}

fun buyLotto(gameTimes: Int, numberCombinationList: List<NumberCombination> = emptyList()): List<Lotto> {
val autoTimes = LottoCalculator.getAutoTimes(gameTimes, numberCombinationList.size)
val lottoList = mutableListOf<Lotto>()

lottoList.addAll(numberCombinationList.map { createManualLotto(it) })
lottoList.addAll(createLotto(autoTimes))

return lottoList
}

return createLotto(times)
fun getWinningStats(winningLotto: WinningLotto, purchaseLottoList: List<Lotto>): EnumMap<LottoRanking, Int> {
return WinningDomain.checkWinningResult(winningLotto, purchaseLottoList)
}

private fun createLotto(times: Int): List<Lotto> {
Expand All @@ -26,8 +40,12 @@ class LottoGame(private val randomLogic: RandomLogicInterface) {
return lottoList
}

fun getWinningStats(winningLotto: WinningLotto, purchaseLottoList: List<Lotto>): Map<LottoRanking, Int> {
private fun createManualLotto(numberCombination: NumberCombination): Lotto {
val lottoNumberCombination = LottoNumber.createLottoNumbers(numberCombination)
return LottoMachine.createSelectLotto(lottoNumberCombination)
}

return WinningDomain.checkWinningResult(winningLotto, purchaseLottoList)
companion object {
private const val DEFAULT_COST = 1000
}
}
38 changes: 34 additions & 4 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
package lotto.view

import lotto.data.NumberCombination

object InputView {

private const val ERR_MSG_INVALID_NUMERIC_FORMAT = "입력된 값의 포맷이 숫자가 압니다."
private const val ERR_MSG_OUT_OF_CASH_RANGE = "입력된 값이 구매 가능할 수 있는 금액의 범위를 벗어났습니다"
private const val MIN_GAME_COST = 1000
private const val MAX_GAME_COST = 100000

fun inputCash(): String {
fun inputCash(): Int {
println("구입금액을 입력해 주세요.")
val inputData = readln()
validateCash(inputData)
return inputData
return inputData.toInt()
}

fun inputManualCnt(gameTimes: Int): Int {
println("수동으로 구매할 로또 수를 입력해 주세요.")
val inputData = readln()
validateBuyManual(inputData, gameTimes)
return inputData.toInt()
}

fun inputWinningNumber(): List<Int> {
fun inputNumberCombination(manualGameTimes: Int): List<NumberCombination> {
val numberCombinationList = mutableListOf<NumberCombination>()

println("수동으로 구매할 번호를 입력해 주세요.")
repeat(manualGameTimes) {
val inputData = readln()
numberCombinationList.add(NumberCombination(splitInputData(inputData)))
}
println()
return numberCombinationList.toList()
}

fun inputWinningNumber(): NumberCombination {
println("지난 주 당첨 번호를 입력해 주세요.")
val inputData = readln()
println()
return splitInputData(inputData)
return NumberCombination(splitInputData(inputData))
}

fun inputBonusNumber(): Int {
Expand All @@ -33,6 +54,15 @@ object InputView {
validateNumberRange(inputData.toInt())
}

fun validateBuyManual(inputData: String, gameTimes: Int) {
validateNumericFormat(inputData)
validateManualTimes(inputData.toInt(), gameTimes)
}

private fun validateManualTimes(inputData: Int, gameTimes: Int) {
require(gameTimes >= inputData) { "수동 게임 횟수가 총 게임 횟수를 초과하였습니다." }
}

fun splitInputData(inputData: String): List<Int> {
val splitData = inputData.split(",").map { it.trim() }
validateWinningNumber(splitData)
Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/lotto/view/OutputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lotto.view

import lotto.data.Lotto
import lotto.data.LottoRanking
import lotto.domain.LottoCalculator

object OutputView {

Expand All @@ -10,7 +11,10 @@ object OutputView {
private const val TXT_LOSS_COMMENT = "기준이 1이기 때문에 결과적으로 손해라는 의미임"
private const val DIVIDING_LINE = "---------"

fun showLottoList(lottoList: List<Lotto>) {
fun showLottoList(lottoList: List<Lotto>, manualGameTimes: Int) {
val autoGameTimes = LottoCalculator.getAutoTimes(lottoList.size, manualGameTimes)

println("수동으로 ${manualGameTimes}장, 자동으로 ${autoGameTimes}장 구매했습니다.")
lottoList.forEach {
println(it.selectNumbers)
}
Expand Down
20 changes: 11 additions & 9 deletions src/test/kotlin/lotto/LottoGameTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lotto
import lotto.data.Lotto
import lotto.data.LottoNumber
import lotto.data.LottoRanking
import lotto.data.NumberCombination
import lotto.domain.LottoMachine
import lotto.domain.RandomLogic
import lotto.service.LottoGame
Expand All @@ -15,9 +16,10 @@ class LottoGameTest {
// given : 금액을 입력 받는다.
val cash = 4500
val lottoGame = LottoGame(RandomLogic())
val gameTimes = lottoGame.getGameTimes(cash)

// when : 로또를 구매한다.
val lottoList = lottoGame.buyLotto(cash)
val lottoList = lottoGame.buyLotto(gameTimes)

// then : 입력 받은 금액의 최대 수량의 로또를 구매한다.
assertThat(lottoList.size).isEqualTo(4)
Expand All @@ -27,16 +29,16 @@ class LottoGameTest {
fun `로또 구매와 당첨 번호를 입력 했다면, 당첨을 확인을 요청할 때, 당첨 통계를 반환한다`() {
// given : 로또 구매와 당첨 번호를 입력한다.
// 2등 - 2개, 3등 - 1개, 4등 - 1개
val winningNumberList = listOf(1, 2, 3, 4, 5, 6)
val winningNumberCombination = NumberCombination(listOf(1, 2, 3, 4, 5, 6))
val bonusNumber = 7
val winningLotto = LottoMachine.createWinningLotto(winningNumberList, bonusNumber)
val winningLotto = LottoMachine.createWinningLotto(winningNumberCombination, bonusNumber)

val purchaseLottoNumbers1 = LottoNumber.createLottoNumbers(listOf(1, 2, 3, 4, 5, 7))
val purchaseLottoNumbers2 = LottoNumber.createLottoNumbers(listOf(1, 2, 3, 4, 5, 7))
val purchaseLottoNumbers3 = LottoNumber.createLottoNumbers(listOf(1, 2, 3, 4, 7, 8))
val purchaseLottoNumbers4 = LottoNumber.createLottoNumbers(listOf(1, 2, 3, 7, 8, 9))
val purchaseLottoNumbers5 = LottoNumber.createLottoNumbers(listOf(1, 2, 6, 7, 8, 9))
val purchaseLottoNumbers6 = LottoNumber.createLottoNumbers(listOf(11, 12, 13, 14, 15, 16))
val purchaseLottoNumbers1 = LottoNumber.createLottoNumbers(NumberCombination(listOf(1, 2, 3, 4, 5, 7)))
val purchaseLottoNumbers2 = LottoNumber.createLottoNumbers(NumberCombination(listOf(1, 2, 3, 4, 5, 7)))
val purchaseLottoNumbers3 = LottoNumber.createLottoNumbers(NumberCombination(listOf(1, 2, 3, 4, 7, 8)))
val purchaseLottoNumbers4 = LottoNumber.createLottoNumbers(NumberCombination(listOf(1, 2, 3, 7, 8, 9)))
val purchaseLottoNumbers5 = LottoNumber.createLottoNumbers(NumberCombination(listOf(1, 2, 6, 7, 8, 9)))
val purchaseLottoNumbers6 = LottoNumber.createLottoNumbers(NumberCombination(listOf(11, 12, 13, 14, 15, 16)))

val purchaseLotto1 = Lotto((purchaseLottoNumbers1))
val purchaseLotto2 = Lotto((purchaseLottoNumbers2))
Expand Down
12 changes: 7 additions & 5 deletions src/test/kotlin/lotto/data/LottoNumberTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@ class LottoNumberTest {
// given :

// when : 번호 조합을 입력 받았을 때
val selectNumberList = listOf(1, 2, 3, 4, 5, 6)
val numberCombination = NumberCombination(listOf(1, 2, 3, 4, 5, 6))

// then : 로또 번호 조합을 생성할 수 있다.
val lottoNumbers = LottoNumber.createLottoNumbers(selectNumberList)
val expect = LottoNumber.createLottoNumbers(listOf(1, 2, 3, 4, 5, 6))
val lottoNumbers = LottoNumber.createLottoNumbers(numberCombination)

val expectNumberCombination = NumberCombination(listOf(1, 2, 3, 4, 5, 6))
val expect = LottoNumber.createLottoNumbers(expectNumberCombination)

assertThat(lottoNumbers).isEqualTo(expect)
}

@Test
fun `1 ~ 45 범위를 넘어가는 값을 받았다면, 로또를 생성했을 때, 예외를 던진다`() {
// given : 범위를 벗어나는 값을 포함하여 번호를 구성한다.
val selectNumberList = listOf(1, 2, 3, 4, 5, 450)
val numberCombination = NumberCombination(listOf(1, 2, 3, 4, 5, 450))

// when : 로또번호를 구성한다.
val actual = runCatching { LottoNumber.createLottoNumbers(selectNumberList) }.exceptionOrNull()
val actual = runCatching { LottoNumber.createLottoNumbers(numberCombination) }.exceptionOrNull()

// then : 예외를 던진다.
assertThat(actual).isInstanceOf(IllegalArgumentException()::class.java)
Expand Down
Loading