-
Notifications
You must be signed in to change notification settings - Fork 119
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
[자동차 경주] 이지은 미션 제출합니다. #92
base: main
Are you sure you want to change the base?
Changes from all commits
692eedc
6d589c9
b63d33e
67c6b08
ba42179
db4006b
9d419aa
7a0e5e8
b63257d
dfe1885
4591953
6c2836f
041df0d
2b65ab7
1913e6e
127ef0e
421355e
53fb097
8278c49
e138b07
78ae6c7
f1f00a7
7c7ea82
2a3934b
099f569
d68dd49
6d3f45a
305bf6f
e57a824
a0aa107
87814f2
9cd3d3e
2317482
1216c00
8cec194
57633b8
d02cf8b
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 |
---|---|---|
@@ -1 +1,71 @@ | ||
# kotlin-racingcar-precourse | ||
# 프리코스 2주차 _ 자동차 경주 | ||
|
||
## 자동차 경주 | ||
|
||
📍구현할 기능 목록 | ||
1. 경주할 자동차 이름 입력받기 | ||
2. 입력받은 자동차 이름 유효성 검증 | ||
|
||
- 잘못된 값을 입력할 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션 종료 | ||
1. 이름을 5자 초과하여 입력한 경우 | ||
2. 빈 값을 입력한 경우 | ||
3. 쉼표가 아닌 구분자를 사용한 경우 (구분자를 쉼표(,)만 인식하도록 하여 하나의 문자열로 처리) | ||
4. 자동차 이름이 중복된 경우 | ||
|
||
3. 시도할 횟수 입력받기 | ||
4. 입력받은 시도 횟수 유효성 검증 | ||
|
||
- 잘못된 값을 입력할 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션 종료 | ||
1. 음수를 입력한 경우 | ||
2. 0을 입력한 경우 | ||
3. 문자열을 입력한 경우 | ||
4. 입력한 수가 Int 범위를 벗어나는 경우 | ||
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. integer 범위를 벗어나는 경우는 생각 못했네요!! |
||
|
||
5. 0부터 9 사이의 랜덤값을 통해 전진 유무를 결정 | ||
6. 입력받은 시도 횟수만큼 반복 | ||
7. 실행 결과 출력 | ||
8. 전진 횟수가 가장 많은 자동차 판별 | ||
9. 최종 우승자 출력 | ||
|
||
|
||
## 코드 구조 | ||
MVC (모델 - 뷰 - 컨트롤러) 패턴을 사용하여 코드 구조 설계 | ||
|
||
### Model | ||
1. Car | ||
- 역할 : 자동차의 상태와 동작을 정의 | ||
- 주요 메서드 | ||
- `move()` : 자동차가 전진할 때 호출되는 메서드로 거리를 증가시킴 | ||
2. Game | ||
- 역할 : 자동차 경주 게임의 상태를 관리 | ||
- 주요 메서드 | ||
- `playRound()` : 각 자동차가 이동하는 게임 라운드를 실행 | ||
- `getCars()` : 현재 게임에 참여하고 있는 자동차 리스트를 반환 | ||
- `getWinners()` : 현재까지의 라운드에서 가장 멀리 이동한 자동차를 찾아 그 이름을 리스트로 반환 | ||
- `isMoveForward()` : Ramdon값을 사용하여 자동차의 이동 여부를 결정 | ||
|
||
### View | ||
1. InputView | ||
- 역할 : 사용자로부터 입력을 받고, 해당 입력의 유효성을 검증 | ||
- 주요 메서드 | ||
- `inputCarNames()` : 사용자에게 자동차 이름을 입력받음 | ||
- `inputRounds()` : 사용자에게 시도할 라운드 수를 입력받음 | ||
- `validateEmptyInput()` : 입력이 비어있거나 공백만 포함된 경우 유효성 검사 | ||
- `validateSeparator()` : 입력 문자열이 유효한 쉼표 구분 형식인지 확인 | ||
- `validateCarNames()` : 자동차 이름의 유효성을 검사하는 메서드로, 이름의 길이와 중복 여부를 확인 | ||
- `validateNameLength()` : 자동차 이름의 길이가 1자 이상 5자 이하인지 확인 | ||
- `validateDuplicateNames()` : 자동차 이름 리스트에서 중복된 이름이 없는지 확인 | ||
- `splitAndTrimNames()` : 입력 문자열을 쉼표로 분리하고, 각 이름의 앞뒤 공백을 제거하여 리스트로 반환 | ||
- `parseRounds()` : 입력 문자열을 정수로 변환하고, 변환된 값이 유효한 라운드 수인지 확인 | ||
- `validatePositiveRounds()` : 라운드 수가 1 이상, 정수 범위 내에 있는지 확인 | ||
2. ResultView | ||
- 역할 : 자동차 경주 게임의 결과를 출력 | ||
- 주요 메서드 | ||
- `printRoundResult()` : 각 자동차의 이름과 이동 거리를 출력 | ||
- `printWinners()` : 최종 우승자 목록을 출력 | ||
|
||
### Controller | ||
1. RacingGameController | ||
- 역할 : 자동차 경주 게임의 주요 로직을 관리하며 View와 Model 간의 상호작용을 조정 | ||
- 주요 메서드 | ||
- `startGame()` : 게임을 시작하는 메서드 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
package racingcar | ||
|
||
import racingcar.controller.RacingGameController | ||
|
||
fun main() { | ||
// TODO: 프로그램 구현 | ||
} | ||
val controller = RacingGameController() | ||
controller.startGame() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package racingcar.controller | ||
|
||
import racingcar.model.Game | ||
import racingcar.view.InputView | ||
import racingcar.view.ResultView | ||
|
||
class RacingGameController( | ||
private val inputView: InputView = InputView(), | ||
private val resultView: ResultView = ResultView(), | ||
private val racingGame: Game = Game(inputView.inputCarNames()) | ||
) { | ||
fun startGame() { | ||
val rounds = inputView.inputRounds() | ||
|
||
repeat(rounds) { | ||
racingGame.playRound() | ||
resultView.printRoundResult(racingGame.getCars()) | ||
} | ||
|
||
val winners = racingGame.getWinners() | ||
resultView.printWinners(winners) | ||
} | ||
Comment on lines
+12
to
+22
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. 이렇게 보니 컨트롤러의 책임을 최소화하여 뷰와 도메인의 중재자 역할만 하게 하는 것도 전체적인 흐름을 파악하는데에 어려움이 전혀 없네요. 제가 작성한 것보다 훨씬 구조를 파악하기 쉽고 책임 분리가 잘 된 객체지향적인 구현인 것 같습니다. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package racingcar.model | ||
|
||
class Car(val name: String) { | ||
var distance: Int = 0 | ||
private set | ||
|
||
fun move() { | ||
distance++ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,35 @@ | ||||||||||||
package racingcar.model | ||||||||||||
|
||||||||||||
import camp.nextstep.edu.missionutils.Randoms | ||||||||||||
|
||||||||||||
class Game(carNames: List<String>) { | ||||||||||||
private val cars = carNames.map { Car(it) } | ||||||||||||
|
||||||||||||
fun playRound() { | ||||||||||||
cars.forEach { car -> | ||||||||||||
if (isMoveForward()) { | ||||||||||||
car.move() | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
private fun isMoveForward(): Boolean { | ||||||||||||
return Randoms.pickNumberInRange(MIN_MOVE_THRESHOLD, MAX_MOVE_THRESHOLD) >= MOVE_THRESHOLD | ||||||||||||
} | ||||||||||||
|
||||||||||||
fun getCars(): List<Car> { | ||||||||||||
return cars | ||||||||||||
} | ||||||||||||
|
||||||||||||
fun getWinners(): List<String> { | ||||||||||||
val maxDistance = cars.maxOf { it.distance } | ||||||||||||
|
||||||||||||
return cars.filter { it.distance == maxDistance }.map { it.name } | ||||||||||||
Comment on lines
+25
to
+27
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. 바로 리턴해서 코드가 간결하게 표현하는 점도 좋지만, 이렇게 변수를 선언 후 값을 할당해서 리턴하는 방법을 통하면 좀 더 명확해지고, 디버깅시 이점이 있다고 합니다~!😀 아마도 취향차이이지 않으까 싶기도 하네요~
Suggested change
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. 간결한 코드들은 바로 리턴해주는 습관이 있었는데 값을 할당하면 이런 장점들이 있군요 ㅎㅎ 이런 부분도 더 신경을 써봐야겠네요 |
||||||||||||
} | ||||||||||||
|
||||||||||||
companion object { | ||||||||||||
const val MOVE_THRESHOLD = 4 | ||||||||||||
const val MIN_MOVE_THRESHOLD = 0 | ||||||||||||
const val MAX_MOVE_THRESHOLD = 9 | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package racingcar.view | ||
|
||
import camp.nextstep.edu.missionutils.Console | ||
|
||
class InputView { | ||
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. View는 사용자에게 보여지는 화면의 기능만을 해야한다고 생각하기에 Validator와 Parser의 기능의 경우 다른 클래스로 분리하는 것이 좋을 것 같습니다. 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. 다음 주차엔 조금 더 세부적으로 나눠보는 연습을 해야겠네요 ㅎㅎ 피드백 감사합니다 ! |
||
|
||
fun inputCarNames(): List<String> { | ||
println(MESSAGE_ENTER_CAR_NAMES) | ||
val input = Console.readLine().orEmpty() | ||
validateEmptyInput(input) | ||
validateSeparator(input) | ||
|
||
val names = splitAndTrimNames(input) | ||
validateCarNames(names) | ||
|
||
return names | ||
} | ||
|
||
fun inputRounds(): Int { | ||
println(MESSAGE_ENTER_ROUNDS) | ||
val input = Console.readLine().orEmpty() | ||
println() | ||
|
||
return parseRounds(input) | ||
} | ||
|
||
private fun validateEmptyInput(input: String) { | ||
if (input.isBlank()) throw IllegalArgumentException(ERROR_EMPTY_INPUT) | ||
} | ||
|
||
private fun validateSeparator(input: String) { | ||
val names = input.split(COMMA).map { it.trim() } | ||
require(names.isNotEmpty() && names.all { it.isNotBlank() }) { | ||
ERROR_INVALID_SEPARATOR | ||
} | ||
} | ||
|
||
private fun splitAndTrimNames(input: String): List<String> { | ||
return input.split(COMMA).map { it.trim() } | ||
} | ||
|
||
private fun validateCarNames(names: List<String>) { | ||
validateNameLength(names) | ||
validateDuplicateNames(names) | ||
validateNumericNames(names) | ||
} | ||
|
||
private fun validateNameLength(names: List<String>) { | ||
require(names.all { it.isNotBlank() && it.length <= MAX_NAME_LENGTH }) { | ||
ERROR_NAME_LENGTH | ||
} | ||
} | ||
|
||
private fun validateDuplicateNames(names: List<String>) { | ||
require(names.distinct().size == names.size) { | ||
ERROR_DUPLICATE_NAMES | ||
} | ||
} | ||
|
||
private fun validateNumericNames(names: List<String>) { | ||
require(names.none { it.all { char -> char.isDigit() } }) { | ||
ERROR_NUMERIC_NAMES | ||
} | ||
} | ||
|
||
|
||
private fun parseRounds(input: String): Int { | ||
val rounds = input.toIntOrNull() ?: throw IllegalArgumentException(ERROR_INVALID_NUMBER) | ||
validatePositiveRounds(rounds) | ||
|
||
return rounds | ||
} | ||
|
||
private fun validatePositiveRounds(rounds: Int) { | ||
require(rounds in 1..Int.MAX_VALUE) { ERROR_NON_POSITIVE_ROUNDS } | ||
} | ||
|
||
companion object { | ||
const val MESSAGE_ENTER_CAR_NAMES = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" | ||
const val MESSAGE_ENTER_ROUNDS = "시도할 횟수는 몇 회인가요?" | ||
const val ERROR_EMPTY_INPUT = "자동차 이름을 입력해야 합니다." | ||
const val ERROR_INVALID_SEPARATOR = "자동차 이름은 쉼표(,)로 구분해야 합니다." | ||
const val ERROR_NAME_LENGTH = "자동차 이름은 1자 이상 5자 이하여야 합니다." | ||
const val ERROR_DUPLICATE_NAMES = "자동차 이름은 중복될 수 없습니다." | ||
const val ERROR_INVALID_NUMBER = "이동 횟수는 숫자로 입력해야 합니다." | ||
const val ERROR_NON_POSITIVE_ROUNDS = "입력한 이동 횟수가 범위를 벗어났습니다." | ||
const val ERROR_NUMERIC_NAMES = "자동차 이름은 숫자일 수 없습니다." | ||
const val COMMA = "," | ||
const val MAX_NAME_LENGTH = 5 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package racingcar.view | ||
|
||
import racingcar.model.Car | ||
|
||
class ResultView { | ||
|
||
fun printRoundResult(cars: List<Car>) { | ||
cars.forEach { car -> | ||
println("${car.name} : ${"-".repeat(car.distance)}") | ||
} | ||
println() | ||
} | ||
|
||
fun printWinners(winners: List<String>) { | ||
println("$MESSAGE_WINNERS: ${winners.joinToString(", ")}") | ||
} | ||
|
||
companion object { | ||
const val MESSAGE_WINNERS = "최종 우승자" | ||
} | ||
} |
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.
어딘가에서 전해들은 팁인데 체크리스트로 요구사항을 명세하고 해당 기능을 구현한 후의 커밋에 readme의 체크리스트를 [x]로 수정하여 제출하는 방식을 권장한다고 했었던 것 같아요.
저도 이 내용을 저번 주차 이후에 듣고 적용해보았더니 요구사항을 실수로 놓치거나 하는 일이 안 생겨서 좋더라구요.
괜찮다는 방식이란 생각이 드시면 지은님께서도 한번 이 방식을 적용해보시면 좋을 것 같아요!!
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.
이슈나 PR에서만 체크리스트 방식을 생각했었는데 그런 방식도 있겠군요! 공유 감사합니다 :)