-
Notifications
You must be signed in to change notification settings - Fork 201
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] - 재구현 스터디 #238
base: main
Are you sure you want to change the base?
Changes from all commits
41c763b
37ce08e
a6fb74c
d3fdf52
01541c9
ffce85e
04a736f
2cb304b
4bfc5b6
fe03000
b967b63
7c5bb4b
ebd4c52
17bdc6d
51c8355
169e34e
a804f43
9bc16c7
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,24 @@ | ||
# 시스템의 핵심 기능 | ||
|
||
자동차들이 여러 try 동안 전진 조건에 따라 이동하고, 최종 우승자를 얻어낸다. | ||
|
||
이 중에서도 더 핵심은 자동차들이 전진 조건에 따라 이동한다. | ||
|
||
# 구현할 기능 목록 | ||
|
||
## 핵심 비즈니스 | ||
|
||
* 자동차들이 이동한다. | ||
* 자동차의 전진 조건을 검사한다. | ||
* 자동차가 이동한다. | ||
* 자동차들의 위치에 따라 최종 자동차 게임의 우승자를 알아낸다. | ||
|
||
## 입출력 | ||
* 자동차 이름을 입력받는다. | ||
* 시도할 횟수를 입력받는다. | ||
|
||
* 자돋차의 위치를 출력한다. 형식은 `pobi : --` | ||
* 최종 우승자를 안내하는 문구를 출력한다. | ||
* 그 외 안내 메시지, 프폼프트를 출력한다. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,10 @@ | ||
package racingcar | ||
|
||
import racingcar.controller.GameController | ||
import racingcar.model.RandomNumberMoveStrategy | ||
|
||
fun main() { | ||
// TODO: 프로그램 구현 | ||
val gameController = GameController(RandomNumberMoveStrategy()) | ||
gameController.start() | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package racingcar.controller | ||
|
||
import racingcar.model.Cars | ||
import racingcar.model.MoveStrategy | ||
import racingcar.view.InputView | ||
import racingcar.view.OutputView | ||
|
||
class GameController(private val moveStrategy: MoveStrategy) { | ||
|
||
private val inputView = InputView() | ||
private val outputView = OutputView() | ||
|
||
fun start() { | ||
val cars = (inputView.readCars()).toCars() | ||
val tryCount = inputView.readTryCount() | ||
|
||
outputView.showResultHeader() | ||
moveCars(tryCount, cars) | ||
outputView.showTotalWinner(cars.getWinners()) | ||
} | ||
|
||
private fun moveCars(tryCount: Int, cars: Cars) { | ||
repeat(tryCount) { | ||
cars.cars.forEach { car -> | ||
moveStrategy.moveForward(car) | ||
} | ||
outputView.showTryResult(cars) | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package racingcar.controller | ||
|
||
import racingcar.model.Cars | ||
|
||
fun String.toCars(): Cars { | ||
require(this.isNotBlank()) { INPUT_CARS_BLANK } | ||
|
||
val list = this.split(INPUT_CARS_DELIMITERS).map { it.trim() } | ||
return Cars.withNames(*list.toTypedArray()) | ||
} | ||
|
||
const val INPUT_CARS_BLANK = "경주할 자동차 이름을 입력하지 않았습니다." | ||
const val INPUT_CARS_DELIMITERS = ',' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package racingcar.model | ||
|
||
data class Car( | ||
val name: String, | ||
private var _position: Int = 0 | ||
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. 저는 Car가 위치를 알고 있을 필요가 없다고 생각을 했어요 Car의 책임을 전진하거나 멈추거나로 생각을 했어서 이부분은 자동차의 책임을 어디까지 보는가에 따라서 달라질 것 같긴하네요. 그러나, position을 get()으로 내보내는 것 보다 저는 심판(가명)측에서 알고 바로 처리를하는것이 좋을 것 같다는 생각을 했습니다! |
||
) { | ||
init { | ||
require(name.isNotBlank()) { BLANK_NAME } | ||
require(name.length <= NAME_MAX_LENGTH) { TOO_LONG_NAME } | ||
} | ||
|
||
val position: Int | ||
get() = _position | ||
|
||
fun moveForward() { | ||
_position++ | ||
} | ||
|
||
companion object { | ||
private const val NAME_MAX_LENGTH = 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. 어 여긴 가시성이 private으로 설정하셨네요? 혹시 그럼 문자열 상수랑 다르게 한 이유가 있으신건가요? 제가 잘못알았던 건가 |
||
const val TOO_LONG_NAME = "자동차의 이름은 ${NAME_MAX_LENGTH}자 이하만 가능합니다." | ||
const val BLANK_NAME = "자동차의 이름은 공백일 수 없습니다." | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package racingcar.model | ||
|
||
data class Cars(val cars: List<Car>) { | ||
init { | ||
require(cars.size == cars.distinct().size) { DUPLICATED_CARS } | ||
} | ||
|
||
fun getWinners(): List<Car> { | ||
val maxPosition = cars.maxOf { it.position } | ||
return cars.filter { it.position == maxPosition } | ||
} | ||
|
||
companion object { | ||
fun withNames(vararg carNames: String): Cars { | ||
val carsList = carNames.map { Car(it) } | ||
return Cars(carsList) | ||
} | ||
|
||
const val DUPLICATED_CARS = "자동차에 중복된 이름이 있습니다." | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package racingcar.model | ||
|
||
interface MoveStrategy { | ||
fun moveForward(car: Car) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package racingcar.model | ||
|
||
import camp.nextstep.edu.missionutils.Randoms.pickNumberInRange | ||
|
||
class RandomNumberMoveStrategy : MoveStrategy { | ||
override fun moveForward(car: Car) { | ||
if (pickNumberInRange(RANDOM_START_RANGE, RANDOM_END_RANGE) >= RANDOM_NUMBER_CONDITION) { | ||
car.moveForward() | ||
} | ||
} | ||
|
||
companion object { | ||
const val RANDOM_START_RANGE = 0 | ||
const val RANDOM_END_RANGE = 9 | ||
const val RANDOM_NUMBER_CONDITION = 4 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package racingcar.view | ||
|
||
import camp.nextstep.edu.missionutils.Console.readLine | ||
|
||
class InputView { | ||
fun readCars(): String { | ||
println(INPUT_CARS_GUIDE_MESSAGE) | ||
return readLine() | ||
} | ||
|
||
fun readTryCount(): Int { | ||
println(INPUT_TRY_COUNT_GUIDE) | ||
val tryCount = readLine().toIntOrNull() ?: -1 | ||
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. require를 잘 사용해주셨는데요! requireNotNull을 활용하면 좀더 명확하게 처리가 가능할 것 같습니다! 저는 원래 inputview에서 try-catch를 했었는데 페어 때 작성하신 코드를 보고 컨트롤러에서 예외를 잡는게 더 좋겠다 생각이 들어서 저도 변경했습니다! |
||
require(tryCount >= 0) | ||
|
||
return tryCount | ||
} | ||
|
||
companion object { | ||
const val INPUT_CARS_GUIDE_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. 저 한가지 어제 배웠던 점이 있는데요 const val의 가시성을 private로 제한 하는게 맞는 것 같습니다. 3주차 테스트코드 부분에서 private const val로 상수를 선언하더라고요 그걸보니 아 가시성제한을 하는게 맞겠구나 생각이 들었습니다! |
||
const val INPUT_TRY_COUNT_GUIDE = "시도할 횟수는 몇 회인가요?" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package racingcar.view | ||
|
||
import racingcar.model.Car | ||
import racingcar.model.Cars | ||
|
||
class OutputView { | ||
|
||
fun showResultHeader() = println(TRY_RESULT_HEADER) | ||
|
||
fun showTryResult(cars: Cars) { | ||
cars.cars.forEach { | ||
println("${it.name} : " + "-".repeat(it.position)) | ||
} | ||
println() | ||
} | ||
|
||
fun showTotalWinner(winners: List<Car>) { | ||
print(TOTAL_WINNER_GUIDE) | ||
repeat(winners.size - 1) { | ||
print("${winners[it].name}, ") | ||
} | ||
println(winners.last().name) | ||
} | ||
|
||
companion object { | ||
const val TRY_RESULT_HEADER = "실행 결과" | ||
const val TOTAL_WINNER_GUIDE = "최종 우승자 : " | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package racingcar.controller | ||
|
||
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.MethodSource | ||
import org.junit.jupiter.params.provider.ValueSource | ||
import racingcar.model.Car | ||
import racingcar.model.Cars | ||
import java.util.stream.Stream | ||
|
||
class InputConverterTest { | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = ["", " ", "\n"]) | ||
fun `경주할 자동차 문자열이 공백이면 예외를 던진다`(inputCars: String) { | ||
val exception = assertThrows<IllegalArgumentException> { inputCars.toCars() } | ||
assertThat(exception.message).isEqualTo(INPUT_CARS_BLANK) | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideNormalInputCars") | ||
fun `입력받은 자동차들 문자열을 Cars 로 변환한다`(inputCars: String, expectedCars: Cars) { | ||
val result = inputCars.toCars() | ||
assertThat(result).isEqualTo(expectedCars) | ||
} | ||
|
||
companion object { | ||
@JvmStatic | ||
fun provideNormalInputCars(): Stream<Arguments> = Stream.of( | ||
Arguments.of( | ||
"pobi ,woni,jun", | ||
Cars( | ||
listOf( | ||
Car("pobi", 0), | ||
Car("woni", 0), | ||
Car("jun", 0), | ||
) | ||
) | ||
), | ||
Arguments.of( | ||
"pobi,woni,jun,simji", | ||
Cars( | ||
listOf( | ||
Car("pobi", 0), | ||
Car("woni", 0), | ||
Car("jun", 0), | ||
Car("simji", 0), | ||
) | ||
) | ||
), | ||
) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package racingcar.model | ||
|
||
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.CsvSource | ||
import org.junit.jupiter.params.provider.ValueSource | ||
import racingcar.model.Car.Companion.BLANK_NAME | ||
import racingcar.model.Car.Companion.TOO_LONG_NAME | ||
|
||
class CarTest { | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = ["123456", "abcdef", "심지 자동차", "πøˆoi9"]) | ||
fun `자동차의 이름이 5자 초과이면 예외를 던진다`(carName: String) { | ||
assertThrowsWithMessage(carName, TOO_LONG_NAME) | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = ["", " ", "\n"]) | ||
fun `자동차의 이름이 공백이나 빈 값이면 예외를 던진다`(carName: String) { | ||
assertThrowsWithMessage(carName, BLANK_NAME) | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = ["a", " a ", "pobi", "woni", "jun", "simji"]) | ||
fun `정상적인 자동차의 이름은 통과한다`(carName: String) { | ||
assertDoesNotThrow { Car(carName) } | ||
} | ||
|
||
@ParameterizedTest | ||
@CsvSource("0, 1", "1, 2", "10, 11", "100, 101") | ||
fun `자동차가 전진한다`(firstPosition: Int, expectedPosition: Int) { | ||
val car = Car("pobi", firstPosition) | ||
car.moveForward() | ||
|
||
assertThat(car.position == expectedPosition) | ||
} | ||
|
||
|
||
private fun assertThrowsWithMessage(carName: String, message: String) { | ||
val exception = assertThrows<IllegalArgumentException> { | ||
Car(carName) | ||
} | ||
assertThat(exception.message).isEqualTo(message) | ||
} | ||
|
||
} |
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.
페어 프로그래밍 했을 때 논의했던 부분이긴 하지만
저는 이후에 구현을 마무리하면서 data class를 Car만 만들었는데 Cars를 만들 필요가 있을까란 생각이 들더라고요
Car class를 만들면 자동차 객체를 생성할 수 있고, 그걸 리스트의 원소로 넣으면 Cars 객체를 생성할 수 있어서 그러면 자연스럽게 cars.cars 와 같은 표현들도 사용하지 않아도 될것 같더라고요
Cars 클래스를 만들지 않는 것에 대해서 어떻게 생각하시나요?