-
Notifications
You must be signed in to change notification settings - Fork 230
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
[코드 리뷰용 PR] - 재구현 스터디 #250
base: main
Are you sure you want to change the base?
Changes from all commits
b380ee1
bc9d9dd
8224533
f096adc
bc5992d
b3fbddb
a22be16
b37ca8e
a3aa77c
e6badf7
8df6aa9
b9701ec
21434dc
e68ee10
40fdfa5
c484f78
3cadfe2
3ed274c
796f037
f81942e
7c75a95
905a9ba
1dedcee
cd16128
765dc92
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,20 @@ | ||
# 핵심 기능 | ||
|
||
세 자리 수를 정답과 비교해서 결과를 알려준다. | ||
|
||
# 기능 요구사항 | ||
|
||
* 세 자리 수(입력수)를 정답과 비교한다. | ||
|
||
* 정답을 생성한다. | ||
|
||
* 입력수를 입력받는다. | ||
|
||
* 비교 결과를 출력한다. | ||
|
||
* 재시작 여부를 입력받는다. | ||
|
||
# 최종 UML | ||
|
||
![img.png](img.png) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,13 @@ | ||
package baseball | ||
|
||
import baseball.controller.GameController | ||
import baseball.model.RandomAnswerGenerator | ||
import baseball.model.Referee | ||
|
||
fun main() { | ||
TODO("프로그램 구현") | ||
val gameController = GameController( | ||
referee = Referee(), | ||
answerGenerator = RandomAnswerGenerator() | ||
) | ||
gameController.start() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package baseball.controller | ||
|
||
import baseball.model.AnswerGenerator | ||
import baseball.model.BaseballNumbers | ||
import baseball.model.Referee | ||
import baseball.view.Command | ||
import baseball.view.InputView | ||
import baseball.view.OutputView | ||
|
||
class GameController( | ||
val referee: Referee, | ||
val answerGenerator: AnswerGenerator | ||
) { | ||
private val inputView = InputView() | ||
private val outputView = OutputView() | ||
|
||
fun start() { | ||
outputView.showStartPrompt() | ||
do { | ||
val answer = answerGenerator.generate() | ||
oneCycleGame(answer) | ||
outputView.showSuccessPrompt() | ||
} while (inputView.readCommand().toCommand() == Command.RESTART) | ||
} | ||
|
||
private fun oneCycleGame(answer: BaseballNumbers) { | ||
do { | ||
val userBaseballNumbers = (inputView.readNumbers()).toBaseballNumbers() | ||
val ballAndStrike = referee.compare(userBaseballNumbers, answer) | ||
outputView.showTurnResult(ballAndStrike) | ||
} while (!ballAndStrike.isSuccess()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package baseball.controller | ||
|
||
import baseball.model.BaseballNumber | ||
import baseball.model.BaseballNumber.Companion.START_NUMBER | ||
import baseball.model.BaseballNumbers | ||
import baseball.model.BaseballNumbers.Companion.NUMBERS_DIGIT | ||
import baseball.view.Command | ||
import baseball.view.InputView.Companion.COMMAND_PROMPT | ||
|
||
|
||
fun Int.toBaseballNumbers(): BaseballNumbers { | ||
require(this >= START_NUMBER) { | ||
INVALID_NUMBERS | ||
} | ||
|
||
var tempNumber = this | ||
val numbers = mutableListOf<BaseballNumber>() | ||
while (tempNumber > 0) { | ||
val digit = tempNumber % 10 | ||
numbers.add(0, BaseballNumber(digit)) | ||
tempNumber /= 10 | ||
} | ||
return BaseballNumbers(numbers) | ||
} | ||
|
||
fun Int.toCommand(): Command { | ||
val command = Command.entries.firstOrNull { it.command == this } | ||
require(command != null) { | ||
INVALID_COMMAND | ||
} | ||
return command | ||
} | ||
|
||
const val INVALID_NUMBERS = "0 이상의 $NUMBERS_DIGIT 자리 수를 입력해주세요." | ||
val INVALID_COMMAND = "잘못된 입력입니다.\n $COMMAND_PROMPT" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package baseball.model | ||
|
||
interface AnswerGenerator { | ||
fun generate(): BaseballNumbers | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package baseball.model | ||
|
||
import baseball.model.BaseballNumbers.Companion.NUMBERS_DIGIT | ||
|
||
data class BallAndStrike( | ||
val strikeCount: Int, | ||
val ballCount: Int | ||
) { | ||
|
||
fun isSuccess() = strikeCount == NUMBERS_DIGIT | ||
|
||
override fun toString(): String { | ||
return when { | ||
(strikeCount > 0 && ballCount > 0) -> | ||
"$ballCount$BALL_SUFFIX $strikeCount$STRIKE_SUFFIX" | ||
|
||
(strikeCount > 0) -> "$strikeCount$STRIKE_SUFFIX" | ||
(ballCount > 0) -> "$ballCount$BALL_SUFFIX" | ||
else -> NOTHING | ||
} | ||
} | ||
|
||
companion object { | ||
const val BALL_SUFFIX = "볼" | ||
const val STRIKE_SUFFIX = "스트라이크" | ||
const val NOTHING = "낫싱" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package baseball.model | ||
|
||
data class BaseballNumber(val number: Int) { | ||
init { | ||
require(number in START_NUMBER..END_NUMBER) { | ||
INVALID_NUMBER_RANGE | ||
} | ||
} | ||
|
||
companion object { | ||
const val START_NUMBER = 1 | ||
const val END_NUMBER = 9 | ||
const val INVALID_NUMBER_RANGE = "숫자는 $START_NUMBER 와 $END_NUMBER 사이의 수여야 합니다." | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package baseball.model | ||
|
||
data class BaseballNumbers(val numbers: List<BaseballNumber>) { | ||
|
||
init { | ||
require(numbers.size == 3) { | ||
INVALID_NUMBERS_SIZE | ||
} | ||
require(numbers.distinct().size == 3) { | ||
NUMBERS_DUPLICATED | ||
} | ||
} | ||
|
||
companion object { | ||
const val NUMBERS_DIGIT = 3 | ||
const val INVALID_NUMBERS_SIZE = "숫자는 $NUMBERS_DIGIT 자리 수가 되어야 합니다." | ||
const val NUMBERS_DUPLICATED = "숫자에 중복된 수가 있습니다." | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package baseball.model | ||
|
||
import baseball.model.BaseballNumber.Companion.END_NUMBER | ||
import baseball.model.BaseballNumber.Companion.START_NUMBER | ||
import baseball.model.BaseballNumbers.Companion.NUMBERS_DIGIT | ||
import camp.nextstep.edu.missionutils.Randoms.pickNumberInRange | ||
|
||
class RandomAnswerGenerator : AnswerGenerator { | ||
override fun generate(): BaseballNumbers { | ||
val numbers = mutableSetOf<BaseballNumber>() | ||
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으로 depth를 줄이셨군요 |
||
while (numbers.size < NUMBERS_DIGIT) { | ||
numbers.add(BaseballNumber(pickNumberInRange(START_NUMBER, END_NUMBER))) | ||
} | ||
return BaseballNumbers(numbers.toList()) | ||
} | ||
} | ||
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. 저도 Generator 인터페이스를 만들어 상속 받는 방식으로 구현할지 고민하다 object로 NumberGenerator를 구현했는데 어떤 과정을 거쳐 이렇게 구현하게 되셨는지 궁금합니다! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package baseball.model | ||
|
||
class Referee { | ||
fun compare(inputNumbers: BaseballNumbers, answer: BaseballNumbers): BallAndStrike { | ||
var strikeCount = 0 | ||
var ballCount = 0 | ||
for (i in 0 until inputNumbers.numbers.size) { | ||
val inputNumber = inputNumbers.numbers[i] | ||
val answerNumber = answer.numbers[i] | ||
|
||
if (inputNumber == answerNumber) { | ||
strikeCount++ | ||
} else if (answer.numbers.any { it == inputNumber }) { | ||
ballCount++ | ||
} | ||
} | ||
|
||
return BallAndStrike(strikeCount, ballCount) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package baseball.view | ||
|
||
enum class Command(val commandName: String, val command: Int) { | ||
RESTART("새로 시작", 1), | ||
EXIT("종료", 2), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package baseball.view | ||
|
||
|
||
import camp.nextstep.edu.missionutils.Console.readLine | ||
|
||
class InputView { | ||
fun readNumbers(): Int { | ||
print(INPUT_NUMBERS_PROMPT) | ||
return readLine().toIntOrNull() ?: -1 | ||
} | ||
|
||
fun readCommand(): Int { | ||
println(COMMAND_PROMPT) | ||
return readLine().toIntOrNull() ?: -1 | ||
} | ||
|
||
companion object { | ||
private const val DELIMITER = " : " | ||
const val INPUT_NUMBERS_PROMPT = "숫자를 입력해주세요$DELIMITER" | ||
|
||
val COMMAND_PROMPT = | ||
"게임을 ${Command.RESTART.commandName}하려면 ${Command.RESTART.command}, " + | ||
"${Command.EXIT.commandName}하려면 ${Command.EXIT.command}를 입력하세요." | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package baseball.view | ||
|
||
import baseball.model.BallAndStrike | ||
|
||
class OutputView { | ||
fun showStartPrompt() = println(START_PROMPT) | ||
fun showSuccessPrompt() = println(SUCCESS_PROMPT) | ||
|
||
fun showTurnResult(ballAndStrike: BallAndStrike) = println(ballAndStrike) | ||
|
||
companion object { | ||
const val START_PROMPT = "숫자 야구 게임을 시작합니다." | ||
const val SUCCESS_PROMPT = "3개의 숫자를 모두 맞히셨습니다! 게임 종료" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package baseball.controller | ||
|
||
import baseball.model.BaseballNumber | ||
import baseball.model.BaseballNumbers | ||
import baseball.view.Command | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.assertThrows | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.Arguments | ||
import org.junit.jupiter.params.provider.CsvSource | ||
import org.junit.jupiter.params.provider.MethodSource | ||
import org.junit.jupiter.params.provider.ValueSource | ||
import java.util.stream.Stream | ||
|
||
class NumbersConverterTest { | ||
|
||
@ParameterizedTest | ||
@CsvSource("123, 1, 2, 3", "324, 3, 2, 4", "281, 2, 8, 1") | ||
fun `입력받은 수를 게임의 숫자 셋으로 변환한다`(inputNumber: Int, firstDigit: Int, secondDigit: Int, thirdDigit: Int) { | ||
val expected = BaseballNumbers( | ||
listOf(BaseballNumber(firstDigit), BaseballNumber(secondDigit), BaseballNumber(thirdDigit)) | ||
) | ||
|
||
val result = inputNumber.toBaseballNumbers() | ||
assertThat(result).isEqualTo(expected) | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = [-10, -9, -1, 0]) | ||
fun `입력받은 수가 0 이하이면, 예외를 던진다`(inputNumber: Int) { | ||
val exception = assertThrows<IllegalArgumentException> { | ||
inputNumber.toBaseballNumbers() | ||
} | ||
assertThat(exception.message).isEqualTo(INVALID_NUMBERS) | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideInputToCommand") | ||
fun `입력받은 수를 커맨드로 변환한다`(inputNumber: Int, expectedCommand: Command) { | ||
val result = inputNumber.toCommand() | ||
assertThat(result).isEqualTo(expectedCommand) | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = [0, 3, 4, -1, -2, 100]) | ||
fun `입력받은 수가 커맨드가 아닌 수라면 예외를 던진다`(inputNumber: Int) { | ||
val exception = assertThrows<IllegalArgumentException> { | ||
inputNumber.toCommand() | ||
} | ||
assertThat(exception.message).isEqualTo(INVALID_COMMAND) | ||
} | ||
|
||
companion object { | ||
@JvmStatic | ||
fun provideInputToCommand(): Stream<Arguments> = Stream.of( | ||
Arguments.of( | ||
1, Command.RESTART | ||
), | ||
Arguments.of( | ||
2, Command.EXIT | ||
), | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package baseball.model | ||
|
||
import baseball.model.BaseballNumber.Companion.INVALID_NUMBER_RANGE | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.assertDoesNotThrow | ||
import org.junit.jupiter.api.assertThrows | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.ValueSource | ||
|
||
class BaseballNumberTest { | ||
@ParameterizedTest | ||
@ValueSource(ints = [0, -1, 10, 100]) | ||
fun `사용자가 입력한 숫자가 1~9 가 아니면 예외를 던진다`(inputNumber: Int) { | ||
val exception = assertThrows<IllegalArgumentException> { | ||
BaseballNumber(inputNumber) | ||
} | ||
assertThat(exception.message).isEqualTo(INVALID_NUMBER_RANGE) | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = [1, 2, 3, 4, 5, 6, 7, 8, 9]) | ||
fun `사용자가 입력한 숫자가 1~9 가 이면 정상이다`(inputNumber: Int) { | ||
assertDoesNotThrow { | ||
BaseballNumber(inputNumber) | ||
} | ||
} | ||
} |
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.
코틀린 코딩 컨벤션에 함수, 메서드의 이름은 동사 또는 동사구로 짓는 것으로 설명하고 있는데 이 메서드 이름은 의도하신 걸까요?
https://kotlinlang.org/docs/coding-conventions.html#choose-good-names