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

[부나] 2단계 자동차 경주 제출합니다. #64

Merged
merged 99 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
4d15ac7
docs: 기능 목록 초기 작성
tmdgh1592 Feb 7, 2023
2fb4547
feat: 최종 우승자 산출 기능 구현
tmdgh1592 Feb 7, 2023
d85e64e
feat: 자동차 이름들 입력 기능 구현
tmdgh1592 Feb 7, 2023
69fe601
feat: 시도 횟수 입력 기능 구현
tmdgh1592 Feb 7, 2023
ca51779
refactor: 조건 숫자, 에러문구 상수화
tmdgh1592 Feb 7, 2023
37462f6
feat: 자동차 랜덤 전진 기능 구현
tmdgh1592 Feb 7, 2023
8808d37
test: 테스트 커밋
tmdgh1592 Feb 8, 2023
1d86cdd
feat: 실행 결과 출력 기능 구현
tmdgh1592 Feb 8, 2023
e1f5fc1
feat: 최종 우승자 산출 기능 구현
tmdgh1592 Feb 8, 2023
118d2cb
docs: 기능 목록 추가
tmdgh1592 Feb 8, 2023
b826b0e
feat: 우승자 출력 기능 구현
tmdgh1592 Feb 8, 2023
972d412
feat: 기능 통합 구현
tmdgh1592 Feb 8, 2023
7b51f8c
test: Car class 테스트 초기 작성
tmdgh1592 Feb 8, 2023
b16841c
test: Validator class 테스트 초기 작성
tmdgh1592 Feb 8, 2023
e724948
test: Validator class 시도 횟수 입력 테스트 초기 작성
tmdgh1592 Feb 8, 2023
1381ae5
test: RacingService class 객체 생성 테스트 작성
tmdgh1592 Feb 8, 2023
d88ea80
feat: 고정 시드 랜덤 유틸 구현
tmdgh1592 Feb 8, 2023
7e42178
refactor: 랜덤 시작, 종료 범위, 이동 확률 상수화
tmdgh1592 Feb 8, 2023
0edf628
test: 최종 우승자 산출 테스트 코드 구현
tmdgh1592 Feb 9, 2023
1ca0cda
test: 최종 우승자 산출 테스트 코드 구현
tmdgh1592 Feb 9, 2023
4d5d28b
test: 자동차 생성 예외 테스트 코드 구현
tmdgh1592 Feb 9, 2023
89bfc95
test: 최종 우승자 산출 예외 테스트 코드 구현
tmdgh1592 Feb 9, 2023
fe11d89
test: 전체 기능 테스트 코드 구현
tmdgh1592 Feb 9, 2023
5d67e4e
feat: 공백 지우는 클래스 구현
tmdgh1592 Feb 9, 2023
3571323
refactor: 메서드명, 길이 10자 이내로 제한하도록 리팩토링
tmdgh1592 Feb 9, 2023
39f2cdd
test: validator 테스트 코드 매개변수 수정
tmdgh1592 Feb 9, 2023
3e46229
docs: .gitkeep 파일 제거
tmdgh1592 Feb 10, 2023
c34e4cc
docs: .editorconfig 파일 마지막에 개행 문자 추가
tmdgh1592 Feb 10, 2023
890bef6
refactor: createCars() 메서드 반환값 표기
tmdgh1592 Feb 10, 2023
8945701
refactor: ktlint에 맞춰 각 파일의 마지막 줄에 개행 문자 추가
tmdgh1592 Feb 10, 2023
3aa878a
refactor: Car의 toString() 메서드 재정의 제거하고 name 프로퍼티에 접근할 수 있도록 수정
tmdgh1592 Feb 10, 2023
74f7af6
refactor: Car 클래스의 getPositionAsDash() 메서드를 제거하고 position 프로퍼티에 접근할 수…
tmdgh1592 Feb 10, 2023
3ab052f
refactor: Random 유틸 클래스를 제거하고 Service에서 로직 구현
tmdgh1592 Feb 10, 2023
ce2e1bb
refactor: BlankRemover 유틸 클래스를 제거하고 Extensions 파일에서 처리하도록 수정
tmdgh1592 Feb 10, 2023
efd5d0c
fix: getRandomProbabilityInRange() 메서드의 Max bound 범위를 1 감소
tmdgh1592 Feb 10, 2023
9282f50
refactor: Dimens, Strings 파일에서 관리하던 상수를 필요한 클래스에서 관리하도록 수정
tmdgh1592 Feb 10, 2023
35dff55
test: testCarMovement() 메서드명을 testCarMovement()로 변경
tmdgh1592 Feb 10, 2023
e45ab6b
refactor: 불필요한 주석 및 코드 제거
tmdgh1592 Feb 10, 2023
4f9ba23
test: 자동차 객체 생성 테스트 코드 구현
tmdgh1592 Feb 10, 2023
07dcf8e
refactor: ApplicationKtTest 테스트 코드 분리하고 Given-When-Then 표기법으로 변경
tmdgh1592 Feb 10, 2023
983fd98
refactor: CarTest 네이밍을 Given-When-Then 표기법으로 변경
tmdgh1592 Feb 10, 2023
e735d17
refactor: RacingServiceTest 네이밍을 Given-When-Then 표기법으로 변경
tmdgh1592 Feb 10, 2023
8a24159
refactor: 자동차 이름의 길이를 Car 클래스 생성시 검증하도록 변경
tmdgh1592 Feb 10, 2023
d9c1cde
refactor: round 범위를 Round 래퍼 클래스에서 검증하도록 변경
tmdgh1592 Feb 10, 2023
3250ce8
refactor: Round 범위 입력값의 숫자 여부 검증을 InputView에서 하도록 변경하고 일반화
tmdgh1592 Feb 10, 2023
c41adbe
feat: CarRepository 클래스 구현
tmdgh1592 Feb 10, 2023
aaca844
feat: 자동차 중복 검증을 CarRepository에서 하도록 변경
tmdgh1592 Feb 10, 2023
385e9b7
feat: 우승자 산출시 Car의 compareTo() 메서드로 비교하던 부분을 각 position 프로퍼티와 비교하도록 변경
tmdgh1592 Feb 10, 2023
7012826
refactor: Dimens 파일에서 관리하던 상수를 필요한 클래스에서 관리하도록 수정
tmdgh1592 Feb 10, 2023
810050b
refactor: getWinners() 메서드 매개변수 제거
tmdgh1592 Feb 10, 2023
0924fe7
refactor: Round 객체 생성시 검증을 require 메서드를 사용하도록 변경
tmdgh1592 Feb 10, 2023
1f76b4e
test: 라운드 객체 생성 테스트 코드 구현
tmdgh1592 Feb 10, 2023
3783a31
fix: CarRepositry에서 car 객체 삽입시 검증을 위해 비교하는 기준을 이름으로 변경하여 에러 수정
tmdgh1592 Feb 10, 2023
f3d42d2
test: 자동차 moveCount 테스트에서 주어진 자동차 객체를 insert하도록 변경
tmdgh1592 Feb 10, 2023
a2441a2
refactor: 불필요한 상수 제거
tmdgh1592 Feb 10, 2023
3966720
refactor: getRandomProbabilityInRange() 메서드명 변경
tmdgh1592 Feb 10, 2023
f1d7a52
refactor: Car의 move() 메서드에 이동 조건값 추가
tmdgh1592 Feb 10, 2023
5a27b6d
test: Car 클래스의 move() 메서드 테스트 코드 구현
tmdgh1592 Feb 10, 2023
cbed4f1
fix: Car 클래스에서 이름 길이 검증시 throw를 두 번 발생시키는 버그 수정
tmdgh1592 Feb 11, 2023
919ba1c
refactor: 자동차 이름 프로퍼티를 CarName 클래스로 분리
tmdgh1592 Feb 11, 2023
7e22dea
refactor: Car 리스트를 Cars 클래스로 감싸서 RacingService에서 관리하도록 변경
tmdgh1592 Feb 11, 2023
322b011
refactor: CarRepository 클래스 제거
tmdgh1592 Feb 11, 2023
b96c5d1
refactor: RacingController 클래스에서 RacingService 생성을 메서드로 분리
tmdgh1592 Feb 11, 2023
3cd5d3b
refactor: removeBlank() 메서드에 반환 타입 표기
tmdgh1592 Feb 11, 2023
b354f94
feat: Winners 도메인 모델 구현
tmdgh1592 Feb 11, 2023
1b9aae3
refactor: 각 도메인 Model에 대해 DTO 구현
tmdgh1592 Feb 11, 2023
a2a3628
refactor: RacingService에서 도메인 Model과 DTO를 변환하고 입출력시 Model 대신 DTO를 의존하…
tmdgh1592 Feb 11, 2023
c1bbd0c
chore: kotlin("test") 라이브러리 제거
tmdgh1592 Feb 12, 2023
0e2a0d6
chore: Mockito 2.21.0 라이브러리 추가
tmdgh1592 Feb 12, 2023
330601f
refactor: Cars에서 randomGenerator를 주입받아 Car의 randomMove() 메서드의 인자로 이동할…
tmdgh1592 Feb 12, 2023
910225c
refactor: CarName 객체 생성시 전달받은 이름에 대해 양 옆 공백을 제거하여 관리하도록 수정
tmdgh1592 Feb 12, 2023
70e8c98
test: CarName 테스트 코드 구현
tmdgh1592 Feb 12, 2023
5f2d1f7
test: Cars 자동차 이름 중복 검증 테스트 구현
tmdgh1592 Feb 12, 2023
feb31e3
test: Cars getWinners() 테스트 코드 구현
tmdgh1592 Feb 12, 2023
2a702a1
refactor: CarTest, RoundTest 패키지 이동
tmdgh1592 Feb 12, 2023
9298caf
refactor: runAllRounds 메서드 인자로 RoundDto 추가
tmdgh1592 Feb 12, 2023
16de727
refactor: MovementProbabilityGenerator generate() 메서드 인자 제거
tmdgh1592 Feb 12, 2023
6320815
refactor: Round 클래스 count 프로퍼티 접근제한자 private 제거
tmdgh1592 Feb 12, 2023
14595f1
test: RacingService 클래스 테스트 코드 구현
tmdgh1592 Feb 12, 2023
249b12d
refactor: merge conflict 해결
tmdgh1592 Feb 12, 2023
9656256
refactor: 자동차 이동 확률 범위를 0에서 9로 변경
tmdgh1592 Feb 13, 2023
ee98952
refactor: Car model의 carName, position 프로퍼티를 var에서 val로 변경
tmdgh1592 Feb 13, 2023
4cf09d2
refactor: Car와 CarDto 변환을 mapper로 분리
tmdgh1592 Feb 13, 2023
ba98fd4
refactor: Cars와 CarsDto 변환을 mapper로 분리
tmdgh1592 Feb 13, 2023
a614450
refactor: Winners와 WinnersDto 클래스 분리
tmdgh1592 Feb 13, 2023
255a8bf
refactor: Round와 RoundDto 변환을 mapper로 분리
tmdgh1592 Feb 13, 2023
228132e
refactor: CarName에서 name 프로퍼티가 _name을 trim()하여 반환하도록 변경
tmdgh1592 Feb 13, 2023
4b82d73
refactor: CarName 클래스의 name 프로퍼티명을 value로 변경
tmdgh1592 Feb 13, 2023
782af39
refactor: RandomGenerator 인터페이스명을 NumberGenerator로 변경
tmdgh1592 Feb 13, 2023
6360209
chore: Mockito 라이브러리 제거
tmdgh1592 Feb 13, 2023
9c847c0
chore: Mockito의 Mock 대신 fake객체를 사용하도록 변경
tmdgh1592 Feb 13, 2023
8046843
refactor: MovementProbabilityGenerator 클래스를 제거하고 CarMoveCondition으로 변경
tmdgh1592 Feb 13, 2023
a39cc01
refactor: MoveStep 인터페이스를 sealed 클래스로 변경
tmdgh1592 Feb 13, 2023
3a09e61
refactor: CarMoveCondition 메서드를 operator invoke 메서드로 변경
tmdgh1592 Feb 13, 2023
78fe97b
refactor: MoveStep 이동 거리 상수화
tmdgh1592 Feb 13, 2023
24e8396
fix: 매 라운드마다 자동차 위치 출력 안 되는 버그 수정
tmdgh1592 Feb 13, 2023
b2919dd
refactor: Car move() 메서드 호출시 Step을 받도록 테스트 코드 변경
tmdgh1592 Feb 13, 2023
dc441fa
refactor: Car moveAll에서 이동 여부 로직을 isSatisfyCondition() 메서드로 분리
tmdgh1592 Feb 13, 2023
8268211
chore: CarRandomMoveCondtion Fake 클래스를 test 패키지로 이동
tmdgh1592 Feb 14, 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
3 changes: 1 addition & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2")
testImplementation("org.assertj", "assertj-core", "3.22.0")
testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3")

testImplementation(kotlin("test"))
testImplementation("org.mockito:mockito-inline:5.1.1")

Choose a reason for hiding this comment

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

mock 라이브러리는 저도 자주 사용하는 라이브러리인데요,
이번 미션에서는 mock라이브러리 사용하지 않고 테스트 코드를 짜보는걸 목표로 해보는게 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

네! 한 번 해보겠습니다 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

추천해주신 것처럼 mock 라이브러리를 제거하고 Fake 객체를 사용하는 방식으로 구현을 시도해보았습니다!

이동 조건이 변경, 추가되었을 때 확장하기 편한 방식으로 구현하다보니, 기존에 RandomGenerator를 제거하고, CarRandomMoveCondition.class를 새롭게 구현하였습니다.

class FakeForSuccess : CarMoveCondition {
        override operator fun invoke(): Int = SUCCESS_NUMBER
    }

    class FakeForFailed : CarMoveCondition {
        override operator fun invoke(): Int = FAIL_NUMBER
    }

구글링을 통해 찾아보니 Fake 객체를 해당 클래스 또는 인터페이스 내에 구현하는 분들이 꽤 많았습니다.
여기서 궁금했던 점은...

  • '우선 이렇게 구현하는게 옳은 방식인가?'
  • 'Fake 객체에 대해서는 테스트를 위한 코드를 작성해도 되는가?'
    입니다..!

구현 방식이 정형화되어 있지 않다보니 어떤게 맞고 틀리다고 할 수 없지만 보편적으로 사용되는 방식이 있는지 궁금해졌습니다!
그리고 테스트를 위한 코드를 지양하라고 알고 있는데 Fake 객체는 그 범주에서 예외인지 의문이 들었습니다.
현직자의 입장에서 의견을 듣고 싶습니다..!

Choose a reason for hiding this comment

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

실제 패키지 내에서 테스트를 위한 코드는 지양하는것이 좋습니다.
저는 Fake객체는 test 패키지 범위에서 관리되기 때문에 실제 구현이라고 보지는 않기도 합니다!
실제 프로젝트에는 객체 생성이 어렵거나, Fake가 힘든 케이스도 있기때문에, Mock 라이브러리를 많이 사용하긴 합니다.
하지만, 실제 간단한 경우에는 Fake객체 생성을 하는 경우도 많습니다 👍

}

tasks {
Expand Down
48 changes: 20 additions & 28 deletions src/main/kotlin/racingcar/controller/RacingController.kt
Original file line number Diff line number Diff line change
@@ -1,63 +1,55 @@
package racingcar.controller

import racingcar.model.Car
import racingcar.model.Round
import racingcar.model.car.CarsDto
import racingcar.model.car.WinnersDto
import racingcar.model.round.RoundDto
import racingcar.service.RacingService
import racingcar.view.InputView
import racingcar.view.OutputView

class RacingController(
private val inputView: InputView = InputView(),
private val outputView: OutputView = OutputView(),
private val racingService: RacingService = RacingService(),
) {
fun runRacing() {
val cars = racingService.createCars(readCarNames())
racingService.insertCars(cars)
private lateinit var racingService: RacingService

fun runRacing() {
initRacingService()
val round = readRound()

runRounds(round.count, cars)
runRounds(round)

val winners = getWinners()
printWinners(winners)
}

private fun readCarNames(): List<String> {
private fun initRacingService() {
racingService = RacingService(readCarNames())
}

private fun readCarNames(): CarsDto {
outputView.printMessage(CAR_NAMES_REQUEST_MESSAGE)
return inputView.readCarNames()
}

private fun readRound(): Round {
private fun readRound(): RoundDto {
outputView.printMessage(ROUND_COUNT_REQUEST_MESSAGE)
return Round(inputView.readNumber())
return inputView.readRound()
}

private fun printRoundCountRequestMessage() = outputView.printMessage(ROUNDS_RESULT_NOTIFICATION_MESSAGE)

private fun printRoundResult(cars: List<Car>) = outputView.printRoundResult(cars)
private fun printRoundResult(cars: CarsDto) = outputView.printRoundResult(cars)

private fun printWinners(winners: List<Car>) = outputView.printWinners(winners)
private fun printWinners(winners: WinnersDto) = outputView.printWinners(winners)

private fun runRounds(roundCount: Int, cars: List<Car>) {
private fun runRounds(round: RoundDto) {
printRoundCountRequestMessage()
repeat(roundCount) {
runRound(cars)
}
}

private fun runRound(cars: List<Car>) {
moveCarsRandomly(cars)
printRoundResult(cars)
}

private fun moveCarsRandomly(cars: List<Car>) {
cars.forEach { car ->
racingService.moveRandomly(car)
racingService.runAllRounds(round) { eachRoundCars ->
printRoundResult(eachRoundCars)
}
}

private fun getWinners(): List<Car> = racingService.getWinners()
private fun getWinners(): WinnersDto = racingService.getWinners()

companion object {
private const val CAR_NAMES_REQUEST_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."
Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/racingcar/model/car/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racingcar.model.car

open class Car(name: String, _position: Int = 0) {
var carName: CarName = CarName(name)
private set

Choose a reason for hiding this comment

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

Suggested change
var carName: CarName = CarName(name)
private set
val carName: CarName = CarName(name)

그냥 불변으로 선언 하셔도 좋을거 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

헉 이런 실수를..!
캐치해주셔서 감사합니다!

var position: Int = _position
private set

Choose a reason for hiding this comment

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

Suggested change
var position: Int = _position
private set
class Car(name: String, private var _position: Int = 0) {
...
val position: Int get() = _position


fun moveRandomly(moveCondition: Int) {
if (moveCondition >= MOVEMENT_PROBABILITY) {
++position
}
}

fun toDto(): CarDto = CarDto(carName.name)

Choose a reason for hiding this comment

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

Car객체에 CarDto에 대한 의존도가 생겼네요,
도메인 레이어의 Car에서는 Controller등에서 사용되는 객체의 존재를 알필요가 있을까요?


companion object {
private const val MOVEMENT_PROBABILITY = 4
}
}

class CarDto(_carName: String, val position: Int = 0) {

Choose a reason for hiding this comment

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

Dto 클래스들을 분리해주세요 😄
레이어별로 다른 패키지에 있는게 구별되기 쉽지않을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

네! Dto와 Domain model을 분리하고 mapper 클래스 파일을 만들어 변환을 돕는 코드로 바꿔 보겠습니다 🙂

var carName: CarName = CarName(_carName.trim())
private set

Choose a reason for hiding this comment

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

변경이 되지않는 변수에는 기본적으로 항상 val을 사용해주세요!


fun toModel(): Car = Car(carName.name, position)
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
package racingcar.model
package racingcar.model.car

class Car(val name: String) {
var position: Int = 0
private set
class CarName(_name: String) {
val name: String

Choose a reason for hiding this comment

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

carName.name이라는 중복적인 name이 등장하게되는데
carName.value같은 변수명은 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

오..! name이 중복되는 네이밍보다 value를 사용하는 것에 훨씬 보기 좋아요!
하나 더 배워갑니다 😆


init {
name = _name.trim()
require(name.length in MIN_CAR_NAME_LENGTH..MAX_CAR_NAME_LENGTH) {
throw IllegalArgumentException(CAR_NAME_LENGTH_OVER_BOUNDARY_ERROR_MESSAGE)
}
}

fun move(condition: Int) {
if (condition >= MOVEMENT_PROBABILITY) {
++position
CAR_NAME_LENGTH_OVER_BOUNDARY_ERROR_MESSAGE
}
}

companion object {
private const val MIN_CAR_NAME_LENGTH = 1
private const val MAX_CAR_NAME_LENGTH = 5
private const val MOVEMENT_PROBABILITY = 4

private const val CAR_NAME_LENGTH_OVER_BOUNDARY_ERROR_MESSAGE =
"자동차 이름 길이의 범위는 $MIN_CAR_NAME_LENGTH 이상 $MAX_CAR_NAME_LENGTH 이하입니다."
Expand Down
40 changes: 40 additions & 0 deletions src/main/kotlin/racingcar/model/car/Cars.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package racingcar.model.car

import racingcar.utils.random.RandomGenerator

class Cars(_cars: List<Car>) : List<Car> by _cars {
init {
validateExistDuplicatedCarName()
}

private fun validateExistDuplicatedCarName() {
val nonDuplicatedCarsForName = this.distinctBy { it.carName.name }

require(this.size == nonDuplicatedCarsForName.size) {
DUPLICATED_CAR_NAME_ERROR_MESSAGE
}
}

fun moveAllRandomly(movementProbabilityGenerator: RandomGenerator): Cars = this.onEach { car ->
val moveProbability = movementProbabilityGenerator.generate()
car.moveRandomly(moveProbability)
}

fun getWinners(): Winners {
val winnerStandard = this.maxBy { it.position }
return Winners(this.filter { it.position == winnerStandard.position })
}

fun toDto(): CarsDto = CarsDto(
this.map { car -> car.toDto() }
)

companion object {
private const val DUPLICATED_CAR_NAME_ERROR_MESSAGE =
"중복된 자동차 이름이 존재합니다."
}
}

class CarsDto(_cars: List<CarDto>) : List<CarDto> by _cars {
fun toModel(): Cars = Cars(this.map { it.toModel() })
}
7 changes: 7 additions & 0 deletions src/main/kotlin/racingcar/model/car/Winners.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package racingcar.model.car

class Winners(winners: List<Car>) : List<Car> by winners {
fun toDto(): WinnersDto = WinnersDto(this)
}

class WinnersDto(winners: List<Car>) : List<Car> by winners
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package racingcar.model
package racingcar.model.round

class Round(val count: Int) {
init {
Expand All @@ -14,3 +14,7 @@ class Round(val count: Int) {
"라운드 횟수의 범위는 $MIN_ROUND_COUNT 이상 $MAX_ROUND_COUNT 이하입니다."
}
}

class RoundDto(val count: Int) {
fun toModel(): Round = Round(count)
}
19 changes: 0 additions & 19 deletions src/main/kotlin/racingcar/repository/CarRepository.kt

This file was deleted.

6 changes: 0 additions & 6 deletions src/main/kotlin/racingcar/repository/Repository.kt

This file was deleted.

43 changes: 15 additions & 28 deletions src/main/kotlin/racingcar/service/RacingService.kt
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
package racingcar.service

import racingcar.model.Car
import racingcar.repository.CarRepository
import racingcar.repository.Repository
import racingcar.model.car.CarsDto
import racingcar.model.car.WinnersDto
import racingcar.model.round.RoundDto
import racingcar.utils.random.MovementProbabilityGenerator
import racingcar.utils.random.RandomGenerator

class RacingService(
private val carRepository: Repository<Car> = CarRepository()
_cars: CarsDto,
private val movementProbabilityGenerator: RandomGenerator = MovementProbabilityGenerator()
) {
fun getAll(): List<Car> = carRepository.selectAll()
private val cars = _cars.toModel()

fun insertCars(cars: List<Car>) {
cars.forEach { insertCar(it) }
fun runAllRounds(round: RoundDto, doEachRoundResult: (CarsDto) -> Unit) {
repeat(round.toModel().count) {
doEachRoundResult(moveCarsRandomly())
}
}

private fun insertCar(car: Car) = carRepository.insert(car)
private fun moveCarsRandomly(): CarsDto =
cars.moveAllRandomly(movementProbabilityGenerator).toDto()

Choose a reason for hiding this comment

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

(이 부분은 정답이 없는 부분이니 참고하셔서 고민만 해주세요 )
현재는 RacingService,Cars에서 generateNumber를 하는 부분의 책임을 인터페이스로 분리시켰지만,
number를 가지고 전진할지 말지에 대한 책임은 어디에 있는것이 좋을까요?
Car가 움직이는 기준이 랜덤번호가 아닌 다른 방법 변경된다면,
Car마다 움직이는 기준이 달라진다면,
Car마다 한번에 이동하는 거리가 달라진다면,
어떤구조가 좀더 유연하게 대처할수 있는 구조가 되고, 가독성이 좋은 직관적인 코드가 될까요?
좋은 코드가 무엇인지에 대해 고민하셔도 좋을거 같아요!

Copy link
Member Author

@tmdgh1592 tmdgh1592 Feb 13, 2023

Choose a reason for hiding this comment

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

이 글을 읽어보고 '어떻게 유지보수에 유연한 코드를 작성할 수 있을까?' 라는 고민을 할 수 있었습니다.
또 어떻게 변경될지에 대해 미리 고려하고 코드를 작성하시는 습관에 감탄했습니다..! 👏

number를 가지고 전진할지 말지에 대한 책임은 어디에 있는것이 좋을까요?
우선, 모든 자동차에 대해 전진할지 말지에 대한 책임은 Cars에 있다고 생각을 했습니다.
또는 Car가 자체적으로도 책임을 가질 수 있다고도 생각해 해보았는데, 개인적으로 cars에서 몇 칸씩 움직일지 car.move() 메서드의 인자로 전달해주면 해당 값만큼 이동하는 것도 방법이라고 생각했습니다.
잭슨님께서 생각하시는 또 다른 방법이 있으시다면 공유해주셨을 때 많은 도움이 될 것 같습니다 :)

Car가 움직이는 기준이 랜덤번호가 아닌 다른 방법 변경된다면,
Car마다 움직이는 기준이 달라진다면,
Car마다 한번에 이동하는 거리가 달라진다면,

말씀해주신 것처럼 주어진 요구사항이 변경되었을 때, 기존 코드를 변경해야 하는 부분이 정말 많이 생긴다는 사실을 알 수 있었습니다.
최대한 코드의 수정을 피하기 위해, Car가 움직이는 기준을 interface로 분리하고 구현체를 주입하는 방식을 떠올려 보았습니다.

// CarMoveCondition
interface CarMoveCondition {
    operator fun invoke(): Int
}

// CarRandomMoveCondition
class CarRandomMoveCondition : CarMoveCondition {
    override operator fun invoke(): Int =
        (START_RANDOM_MOVEMENT_PROBABILITY..END_RANDOM_MOVEMENT_PROBABILITY).random()

// Cars
fun moveAll(carMoveCondition: CarMoveCondition): Cars = this.onEach { car ->
        if (carMoveCondition() >= MOVE_CONDITION) {

이러한 구현 방식으로 리팩토링 하다보니 자동차 이동 조건이 변경되었을 때, CarMoveCondtion을 구현하는 또 다른 조건 클래스를 주입해주기만 하면 되는 구조로 바뀌었습니다!
즉, 기존 코드에 비해 변경에는 닫혀 있고, 확장에는 열려 있는 구조로 만들 수 있었습니다.

Car마다 움직이는 기준이 달라진다면,
이 말씀에 대해서는 Cars가 아닌, Car에게 CarMoveCondtion을 주입해주는 구현 방식을 떠올렸습니다.
그런데 이 의미가 맞는지 확신이 안 서서 아직 구현은 하지 않은 상태입니다..!

Car마다 한번에 이동하는 거리가 달라진다면,
만약 Car마다 이동하는 거리가 달라진다면, 얼마나 이동할지 Car에게 알려주면 된다고 생각했습니다.

sealed class MoveStep {
    abstract fun move(): Int
}

object ZeroStep : MoveStep() {
    private const val ZERO_STEP = 0

    override fun move(): Int = ZERO_STEP
}

object OneStep : MoveStep() {
    private const val ONE_STEP = 1

    override fun move(): Int = ONE_STEP
}

sealed 클래스를 사용하여 몇 칸 움직일지 정하는 부분을 OCP에 위배되지 않게끔 시도해봤습니다.
최대한 잭슨님께서 남겨주신 피드백을 기반으로 의미를 곱씹으면서 리팩토링을 해보았는데, 제가 잘 이해했는지, 구현했는지 아직은 잘 모르겠습니다.. 🥲

긴 글 읽어주셔서 감사합니다!

Choose a reason for hiding this comment

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

👍 👍
사실 소프트웨어 원칙중 YAGNI (You Ain't Gonna Need it)라는 원칙 또한 있습니다.
확장을 예상해서 오버 엔지니링하여 구현하게되면, 예상된 확장이 아닌 경우, 더 안좋은 결과가 나올수도 있기도하는데요,
미션이니 만큼 여러 고민과 도전을 하신부분은 좋습니다 👍 👍
하지만 변경될지에 대해 미리 고려하고 코드는 좋은 방법은 아닐수 있어요!
그렇기 때문에 클래스별로 책임을 나누어 관리하고, 좀더 자유롭게 리팩터링할수 있도록하는, 테스트 코드 기반이 중요하다는 생각이 중요한것 같아요
정말 고생많으셨습니다! 👍


fun createCars(names: List<String>): List<Car> =
names.map { Car(it) }

fun moveRandomly(car: Car) {
car.move(getRandomProbability())
}

private fun getRandomProbability(): Int =
(START_RANDOM_MOVEMENT_PROBABILITY..END_RANDOM_MOVEMENT_PROBABILITY).random()

fun getWinners(): List<Car> {
val cars = getAll()
val winnerStandard = cars.maxBy { it.position }
return cars.filter { it.position == winnerStandard.position }
}

companion object {
private const val START_RANDOM_MOVEMENT_PROBABILITY = 1
private const val END_RANDOM_MOVEMENT_PROBABILITY = 10
}
fun getWinners(): WinnersDto = cars.getWinners().toDto()
}
3 changes: 2 additions & 1 deletion src/main/kotlin/racingcar/utils/Extensions.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package racingcar.utils

fun List<String>.removeBlank() = map { it.trim() }
fun List<String>.removeBlank(): List<String> =
this.map { it.trim() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racingcar.utils.random

class MovementProbabilityGenerator : RandomGenerator {
override fun generate(): Int =
(START_RANDOM_MOVEMENT_PROBABILITY..END_RANDOM_MOVEMENT_PROBABILITY).random()

companion object {
private const val START_RANDOM_MOVEMENT_PROBABILITY = 1
private const val END_RANDOM_MOVEMENT_PROBABILITY = 10
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/racingcar/utils/random/RandomGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.utils.random

interface RandomGenerator {
fun generate(): Int
}

Choose a reason for hiding this comment

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

인터페이스에서는 Random으로 생성된다는 정보를 포함하는게 좋은 방법일까요?
해당 인터페이스의 주요기능은 Number를 generate해주는것이지,
해당 Number가 Random으로 발생하는지, 일정한 Number가 발생하는지는
해당 인터페이스를 구현한 클래스의 책임이지 않을까욤?

Copy link
Member Author

Choose a reason for hiding this comment

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

아.. 그렇군요!
메서드명과 반환타입만 보았을 때, 단순히 Integer 타입의 값을 만들어낸다고밖에 안보이는데 interface명이 Random일 필요가 없겠군요..!
리팩토링시 함께 수정해보겠습니다!

18 changes: 13 additions & 5 deletions src/main/kotlin/racingcar/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package racingcar.view

import racingcar.model.car.CarDto
import racingcar.model.car.CarsDto
import racingcar.model.round.RoundDto
import racingcar.utils.removeBlank

class InputView {
fun readCarNames(): List<String> = readln()
.split(CAR_NAME_DELIMITER)
.removeBlank()
fun readCarNames(): CarsDto = CarsDto(
readln()
.split(CAR_NAME_DELIMITER)
.removeBlank()
.map { carName ->
CarDto(carName)
}
)

fun readNumber(): Int {
fun readRound(): RoundDto {
val number = readln().toIntOrNull()
requireNotNull(number) { NOT_NUMERIC_ERROR_MESSAGE }

return number
return RoundDto(number)
}

companion object {
Expand Down
11 changes: 6 additions & 5 deletions src/main/kotlin/racingcar/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package racingcar.view

import racingcar.model.Car
import racingcar.model.car.CarsDto
import racingcar.model.car.WinnersDto

class OutputView {
fun printMessage(message: String) = println(message)

fun printRoundResult(cars: List<Car>) {
fun printRoundResult(cars: CarsDto) {
cars.forEach { car ->
println("${car.name} : ${getPositionAsDash(car.position)}")
println("${car.carName.name} : ${getPositionAsDash(car.position)}")
}
println()
}

fun printWinners(winners: List<Car>) {
println("$WINNER_NOTIFICATION_MESSAGE: ${winners.joinToString(", ") { it.name }}")
fun printWinners(winners: WinnersDto) {
println("$WINNER_NOTIFICATION_MESSAGE: ${winners.joinToString(", ") { it.carName.name }}")
}

private fun getPositionAsDash(position: Int): String = DASH.repeat(position)
Expand Down
Loading