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

[코드 리뷰용 PR] - 재구현 스터디 #238

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
41c763b
docs(README): 시스템의 핵심기능과 구현할 기능 목록을 정리한다
sh1mj1 Nov 30, 2023
37ce08e
test(MoveStrategyTest): 이동 전략을 검사하는 테스트를 생성한다
sh1mj1 Nov 30, 2023
a6fb74c
feat(RandomNumberMoveStrategy): 난수를 사용하는 이동 전략을 구현한다
sh1mj1 Nov 30, 2023
d3fdf52
feat(Car): 자동차에 대한 유효성 검증과 테스트를 구현한다
sh1mj1 Nov 30, 2023
01541c9
feat(Cars): 자동차들에 대한 유효성 검증과 테스트를 구현한다
sh1mj1 Nov 30, 2023
ffce85e
refactor(Car): 자동차의 위치는 디폴트 패러미터 값 0으로 설정한다
sh1mj1 Nov 30, 2023
04a736f
refactor(Cars): 자동차들의 생성자를 만드는 팩토리 메서드를 추가한다
sh1mj1 Nov 30, 2023
2cb304b
feat(Car): 자동차의 전진을 구현한다
sh1mj1 Nov 30, 2023
4bfc5b6
feat(MoveStrategy): 조건을 판별하고 자동차를 전진시킨다
sh1mj1 Nov 30, 2023
fe03000
feat(Cars.getWinners): 우승자를 찾아내는 기능, 테스트를 구현한다
sh1mj1 Nov 30, 2023
b967b63
test(CarsTest): 테스트 메서드를 여러 케이스를 한번에 테스트하도록 변경한다
sh1mj1 Nov 30, 2023
7c5bb4b
feat(InputView): 입력 기능을 구현한다
sh1mj1 Nov 30, 2023
ebd4c52
feat(InputConverter.toCars): 입력받은 경주할 자동차를 `Cars` 로 변환한다
sh1mj1 Nov 30, 2023
17bdc6d
feat(InputView.readTryCount): 입력받은 시도 횟수에 유효성 검증 로직을 추가한다
sh1mj1 Nov 30, 2023
51c8355
feat(OutputView.showTryResult): 실행 결과를 알려주는 기능을 구현한다
sh1mj1 Nov 30, 2023
169e34e
feat(OutputView.showTotalWinner): 실행 결과를 알려주는 기능을 구현한다
sh1mj1 Nov 30, 2023
a804f43
feat(GameController): 전체 게임을 실행한다
sh1mj1 Nov 30, 2023
9bc16c7
refactor(Car): _position 과 position 을 도입해 외부에서 setter 를 사용할 수 없게 한다
sh1mj1 Dec 1, 2023
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
24 changes: 24 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 시스템의 핵심 기능

자동차들이 여러 try 동안 전진 조건에 따라 이동하고, 최종 우승자를 얻어낸다.

이 중에서도 더 핵심은 자동차들이 전진 조건에 따라 이동한다.

# 구현할 기능 목록

## 핵심 비즈니스

* 자동차들이 이동한다.
* 자동차의 전진 조건을 검사한다.
* 자동차가 이동한다.
* 자동차들의 위치에 따라 최종 자동차 게임의 우승자를 알아낸다.

## 입출력
* 자동차 이름을 입력받는다.
* 시도할 횟수를 입력받는다.

* 자돋차의 위치를 출력한다. 형식은 `pobi : --`
* 최종 우승자를 안내하는 문구를 출력한다.
* 그 외 안내 메시지, 프폼프트를 출력한다.


7 changes: 6 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
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()

}
31 changes: 31 additions & 0 deletions src/main/kotlin/racingcar/controller/GameController.kt
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 ->

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 클래스를 만들지 않는 것에 대해서 어떻게 생각하시나요?

moveStrategy.moveForward(car)
}
outputView.showTryResult(cars)
}
}

}
13 changes: 13 additions & 0 deletions src/main/kotlin/racingcar/controller/InputConverter.kt
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 = ','
24 changes: 24 additions & 0 deletions src/main/kotlin/racingcar/model/Car.kt
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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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 = "자동차의 이름은 공백일 수 없습니다."
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/racingcar/model/Cars.kt
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 = "자동차에 중복된 이름이 있습니다."
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/racingcar/model/MoveStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.model

interface MoveStrategy {
fun moveForward(car: Car)
}
17 changes: 17 additions & 0 deletions src/main/kotlin/racingcar/model/RandomNumberMoveStrategy.kt
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
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
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

Choose a reason for hiding this comment

The 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 = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"

Choose a reason for hiding this comment

The 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 = "시도할 횟수는 몇 회인가요?"
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/racingcar/view/OutputView.kt
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 = "최종 우승자 : "
}
}
56 changes: 56 additions & 0 deletions src/test/kotlin/racingcar/controller/InputConverterTest.kt
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),
)
)
),
)
}

}
49 changes: 49 additions & 0 deletions src/test/kotlin/racingcar/model/CarTest.kt
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)
}

}
Loading