Skip to content

Commit

Permalink
[부나] 2단계 자동차 경주 제출합니다. (#64)
Browse files Browse the repository at this point in the history
* docs: 기능 목록 초기 작성

* feat: 최종 우승자 산출 기능 구현

* feat: 자동차 이름들 입력 기능 구현

* feat: 시도 횟수 입력 기능 구현

* refactor: 조건 숫자, 에러문구 상수화

* feat: 자동차 랜덤 전진 기능 구현

* test: 테스트 커밋

* feat: 실행 결과 출력 기능 구현

* feat: 최종 우승자 산출 기능 구현

* docs: 기능 목록 추가

* feat: 우승자 출력 기능 구현

* feat: 기능 통합 구현

* test: Car class 테스트 초기 작성

* test: Validator class 테스트 초기 작성

* test: Validator class 시도 횟수 입력 테스트 초기 작성

* test: RacingService class 객체 생성 테스트 작성

* feat: 고정 시드 랜덤 유틸 구현

* refactor: 랜덤 시작, 종료 범위, 이동 확률 상수화

* test: 최종 우승자 산출 테스트 코드 구현

* test: 최종 우승자 산출 테스트 코드 구현

* test: 자동차 생성 예외 테스트 코드 구현

* test: 최종 우승자 산출 예외 테스트 코드 구현

* test: 전체 기능 테스트 코드 구현

* feat: 공백 지우는 클래스 구현

* refactor: 메서드명, 길이 10자 이내로 제한하도록 리팩토링

* test: validator 테스트 코드 매개변수 수정

* docs: .gitkeep 파일 제거

* docs: .editorconfig 파일 마지막에 개행 문자 추가

* refactor: createCars() 메서드 반환값 표기

* refactor: ktlint에 맞춰 각 파일의 마지막 줄에 개행 문자 추가

* refactor: Car의 toString() 메서드 재정의 제거하고 name 프로퍼티에 접근할 수 있도록 수정

* refactor: Car 클래스의 getPositionAsDash() 메서드를 제거하고 position 프로퍼티에 접근할 수 있도록 수정

* refactor: Random 유틸 클래스를 제거하고 Service에서 로직 구현

* refactor: BlankRemover 유틸 클래스를 제거하고 Extensions 파일에서 처리하도록 수정

* fix: getRandomProbabilityInRange() 메서드의 Max bound 범위를 1 감소

* refactor: Dimens, Strings 파일에서 관리하던 상수를 필요한 클래스에서 관리하도록 수정

* test: testCarMovement() 메서드명을 testCarMovement()로 변경

* refactor: 불필요한 주석 및 코드 제거

* test: 자동차 객체 생성 테스트 코드 구현

* refactor: ApplicationKtTest 테스트 코드 분리하고 Given-When-Then 표기법으로 변경

* refactor: CarTest 네이밍을 Given-When-Then 표기법으로 변경

* refactor: RacingServiceTest 네이밍을 Given-When-Then 표기법으로 변경

* refactor: 자동차 이름의 길이를 Car 클래스 생성시 검증하도록 변경

* refactor: round 범위를 Round 래퍼 클래스에서 검증하도록 변경

* refactor: Round 범위 입력값의 숫자 여부 검증을 InputView에서 하도록 변경하고 일반화

* feat: CarRepository 클래스 구현

* feat: 자동차 중복 검증을 CarRepository에서 하도록 변경

* feat: 우승자 산출시 Car의 compareTo() 메서드로 비교하던 부분을 각 position 프로퍼티와 비교하도록 변경

* refactor: Dimens 파일에서 관리하던 상수를 필요한 클래스에서 관리하도록 수정

* refactor: getWinners() 메서드 매개변수 제거

* refactor: Round 객체 생성시 검증을 require 메서드를 사용하도록 변경

* test: 라운드 객체 생성 테스트 코드 구현

* fix: CarRepositry에서 car 객체 삽입시 검증을 위해 비교하는 기준을 이름으로 변경하여 에러 수정

* test: 자동차 moveCount 테스트에서 주어진 자동차 객체를 insert하도록 변경

* refactor: 불필요한 상수 제거

* refactor: getRandomProbabilityInRange() 메서드명 변경

* refactor: Car의 move() 메서드에 이동 조건값 추가

* test: Car 클래스의 move() 메서드 테스트 코드 구현

* fix: Car 클래스에서 이름 길이 검증시 throw를 두 번 발생시키는 버그 수정

* refactor: 자동차 이름 프로퍼티를 CarName 클래스로 분리

* refactor: Car 리스트를 Cars 클래스로 감싸서 RacingService에서 관리하도록 변경

* refactor: CarRepository 클래스 제거

* refactor: RacingController 클래스에서 RacingService 생성을 메서드로 분리

* refactor: removeBlank() 메서드에 반환 타입 표기

* feat: Winners 도메인 모델 구현

* refactor: 각 도메인 Model에 대해 DTO 구현

* refactor: RacingService에서 도메인 Model과 DTO를 변환하고 입출력시 Model 대신 DTO를 의존하도록 변경

* chore: kotlin("test") 라이브러리 제거

* chore: Mockito 2.21.0 라이브러리 추가

* refactor: Cars에서 randomGenerator를 주입받아 Car의 randomMove() 메서드의 인자로 이동할 확률을 넘기도록 수정

* refactor: CarName 객체 생성시 전달받은 이름에 대해 양 옆 공백을 제거하여 관리하도록 수정

* test: CarName 테스트 코드 구현

* test: Cars 자동차 이름 중복 검증 테스트 구현

* test: Cars getWinners() 테스트 코드 구현

* refactor: CarTest, RoundTest 패키지 이동

* refactor: runAllRounds 메서드 인자로 RoundDto 추가

* refactor: MovementProbabilityGenerator generate() 메서드 인자 제거

* refactor: Round 클래스 count 프로퍼티 접근제한자 private 제거

* test: RacingService 클래스 테스트 코드 구현

* refactor: 자동차 이동 확률 범위를 0에서 9로 변경

* refactor: Car model의 carName, position 프로퍼티를 var에서 val로 변경

* refactor: Car와 CarDto 변환을 mapper로 분리

* refactor: Cars와 CarsDto 변환을 mapper로 분리

* refactor: Winners와 WinnersDto 클래스 분리

* refactor: Round와 RoundDto 변환을 mapper로 분리

* refactor: CarName에서 name 프로퍼티가 _name을 trim()하여 반환하도록 변경

* refactor: CarName 클래스의 name 프로퍼티명을 value로 변경

* refactor: RandomGenerator 인터페이스명을 NumberGenerator로 변경

* chore: Mockito 라이브러리 제거

* chore: Mockito의 Mock 대신 fake객체를 사용하도록 변경

* refactor: MovementProbabilityGenerator 클래스를 제거하고 CarMoveCondition으로 변경

* refactor: MoveStep 인터페이스를 sealed 클래스로 변경

* refactor: CarMoveCondition 메서드를 operator invoke 메서드로 변경

* refactor: MoveStep 이동 거리 상수화

* fix: 매 라운드마다 자동차 위치 출력 안 되는 버그 수정

* refactor: Car move() 메서드 호출시 Step을 받도록 테스트 코드 변경

* refactor: Car moveAll에서 이동 여부 로직을 isSatisfyCondition() 메서드로 분리

* chore: CarRandomMoveCondtion Fake 클래스를 test 패키지로 이동
  • Loading branch information
tmdgh1592 authored Feb 14, 2023
1 parent 3e4ee27 commit a9bd196
Show file tree
Hide file tree
Showing 31 changed files with 541 additions and 319 deletions.
2 changes: 0 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ 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"))
}

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.dto.car.CarsDto
import racingcar.dto.car.WinnersDto
import racingcar.dto.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
7 changes: 7 additions & 0 deletions src/main/kotlin/racingcar/dto/car/CarDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package racingcar.dto.car

import racingcar.model.car.CarName

class CarDto(_carName: String, val position: Int = 0) {
val carName: CarName = CarName(_carName.trim())
}
3 changes: 3 additions & 0 deletions src/main/kotlin/racingcar/dto/car/CarsDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package racingcar.dto.car

class CarsDto(_cars: List<CarDto>) : List<CarDto> by _cars
5 changes: 5 additions & 0 deletions src/main/kotlin/racingcar/dto/car/WinnersDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.dto.car

import racingcar.model.car.Car

class WinnersDto(winners: List<Car>) : List<Car> by winners
3 changes: 3 additions & 0 deletions src/main/kotlin/racingcar/dto/round/RoundDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package racingcar.dto.round

class RoundDto(val count: Int)
27 changes: 0 additions & 27 deletions src/main/kotlin/racingcar/model/Car.kt

This file was deleted.

12 changes: 12 additions & 0 deletions src/main/kotlin/racingcar/model/car/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package racingcar.model.car

import racingcar.model.car.move.step.MoveStep

class Car(name: String, private var _position: Int = 0) {
val carName: CarName = CarName(name)
val position: Int get() = _position

fun move(moveStep: MoveStep) {
_position += moveStep.move()
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/racingcar/model/car/CarName.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package racingcar.model.car

class CarName(private var _value: String) {
val value: String get() = _value.trim()

init {
require(value.length in MIN_CAR_NAME_LENGTH..MAX_CAR_NAME_LENGTH) {
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 CAR_NAME_LENGTH_OVER_BOUNDARY_ERROR_MESSAGE =
"자동차 이름 길이의 범위는 $MIN_CAR_NAME_LENGTH 이상 $MAX_CAR_NAME_LENGTH 이하입니다."
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/racingcar/model/car/Cars.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package racingcar.model.car

import racingcar.model.car.move.condition.CarMoveCondition
import racingcar.model.car.move.step.OneStep
import racingcar.model.car.move.step.ZeroStep

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

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

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

fun moveAll(carMoveCondition: CarMoveCondition): Cars = this.onEach { car ->
if (isSatisfyCondition(carMoveCondition)) {
car.move(OneStep)
} else {
car.move(ZeroStep)
}
}

private fun isSatisfyCondition(carMoveCondition: CarMoveCondition) =
carMoveCondition() >= MOVE_CONDITION

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

companion object {
private const val DUPLICATED_CAR_NAME_ERROR_MESSAGE =
"중복된 자동차 이름이 존재합니다."
private const val MOVE_CONDITION = 4
}
}
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

import racingcar.dto.car.WinnersDto

class Winners(winners: List<Car>) : List<Car> by winners {
fun toDto(): WinnersDto = WinnersDto(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.model.car.move.condition

interface CarMoveCondition {
operator fun invoke(): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racingcar.model.car.move.condition

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

companion object {
private const val START_RANDOM_MOVEMENT_PROBABILITY = 0
private const val END_RANDOM_MOVEMENT_PROBABILITY = 9
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/racingcar/model/car/move/step/MoveStep.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package racingcar.model.car.move.step

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
}
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 Down
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.

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

import racingcar.model.Car
import racingcar.repository.CarRepository
import racingcar.repository.Repository
import racingcar.dto.car.CarsDto
import racingcar.dto.car.WinnersDto
import racingcar.dto.round.RoundDto
import racingcar.model.car.move.condition.CarMoveCondition
import racingcar.model.car.move.condition.CarRandomMoveCondition
import racingcar.utils.mapper.toDto
import racingcar.utils.mapper.toModel

class RacingService(
private val carRepository: Repository<Car> = CarRepository()
_cars: CarsDto,
private val carMoveCondition: CarMoveCondition = CarRandomMoveCondition()
) {
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(moveCars())
}
}

private fun insertCar(car: Car) = carRepository.insert(car)
private fun moveCars(): CarsDto =
cars.moveAll(carMoveCondition).toDto()

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() }
8 changes: 8 additions & 0 deletions src/main/kotlin/racingcar/utils/mapper/CarMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package racingcar.utils.mapper

import racingcar.dto.car.CarDto
import racingcar.model.car.Car

fun CarDto.toModel(): Car = Car(carName.value, position)

fun Car.toDto(): CarDto = CarDto(carName.value, position)
10 changes: 10 additions & 0 deletions src/main/kotlin/racingcar/utils/mapper/CarsMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar.utils.mapper

import racingcar.dto.car.CarsDto
import racingcar.model.car.Cars

fun CarsDto.toModel(): Cars = Cars(this.map { it.toModel() })

fun Cars.toDto(): CarsDto = CarsDto(
this.map { car -> car.toDto() }
)
6 changes: 6 additions & 0 deletions src/main/kotlin/racingcar/utils/mapper/RoundMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package racingcar.utils.mapper

import racingcar.dto.round.RoundDto
import racingcar.model.round.Round

fun RoundDto.toModel(): Round = Round(count)
Loading

0 comments on commit a9bd196

Please sign in to comment.