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

[자동차 경주] 이현제 미션 제출합니다. #104

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
30dbddc
docs: `README.md` 작성
HyunjeLee Oct 23, 2024
bf650cc
feat: 상수 파일 생성
HyunjeLee Oct 24, 2024
4a7aebd
docs: 중요도에 따른 프로그래밍 요구사항 순서 변경
HyunjeLee Oct 24, 2024
2af0f9f
docs($README): 슈도 코드 추가
HyunjeLee Oct 24, 2024
b798892
docs: 실행 결과 예시 추가
HyunjeLee Oct 25, 2024
e3ea0ea
feat: 자동차 이름 입력 기능
HyunjeLee Oct 26, 2024
2cfaa44
feat: 이동 횟수 입력 기능
HyunjeLee Oct 26, 2024
dd97387
feat: 자동차의 전진 기능
HyunjeLee Oct 27, 2024
85cb6fc
feat: 각 실행 결과 출력 기능
HyunjeLee Oct 27, 2024
70006e7
우승자 출력 기능
HyunjeLee Oct 28, 2024
264b9df
Revert "우승자 출력 기능"
HyunjeLee Oct 28, 2024
26ac6c3
우승자 출력 기능
HyunjeLee Oct 28, 2024
c08ac6a
Revert "우승자 출력 기능"
HyunjeLee Oct 28, 2024
7fbd961
feat: 우승자 출력 기능
HyunjeLee Oct 28, 2024
459bf63
refactor: 자동차의 이름 입력 기능의 for문 의 i -> carName으로 변경
HyunjeLee Oct 28, 2024
2ff2177
feat: 잘못된 입력값에 대해 IllegalArgumentException 발생 기능
HyunjeLee Oct 28, 2024
0fa3033
refactor: 자동차의 이름 입력 요청 함수화
HyunjeLee Oct 28, 2024
df5a4f8
feat: 자동차 이름 입력 중의 공백 제거 기능
HyunjeLee Oct 28, 2024
2992cc8
style: kotlin style에 따른 공백 누락
HyunjeLee Oct 28, 2024
32a34c9
style<promptForCarNames>: 식 함수로 변경
HyunjeLee Oct 28, 2024
f349145
refactor: 자동차 이름 입력값 처리 함수화
HyunjeLee Oct 28, 2024
7c62c2b
refactor: 자동차 이름 유효성 검사 함수화
HyunjeLee Oct 28, 2024
10eef29
refactor: 자동차 이름 입력값을 map으로 변경하는 작업 함수화
HyunjeLee Oct 28, 2024
a63dbf1
refactor: 이동 횟수 입력 요청 함수화
HyunjeLee Oct 28, 2024
f245451
refactor: 이동 횟수 입력값 처리 함수화
HyunjeLee Oct 28, 2024
9dd5110
refactor: 자동차 이름 입력값을 map으로 변경하는 작업 분할
HyunjeLee Oct 28, 2024
5936a82
refactor: 무작위 값을 뽑아 전진 조건을 검사하는 작업 변수화
HyunjeLee Oct 28, 2024
2f7d228
refactor: 모든 실행 결과 출력하는 작업 함수화
HyunjeLee Oct 28, 2024
96d9ce1
refactor: 우승자 출력 작업 함수화
HyunjeLee Oct 28, 2024
efa2cb0
refactor(processCarNames()): 가독성을 위해 return type 명시
HyunjeLee Oct 28, 2024
281d940
refactor: Strings string 관련 상수 파일 변경 및 Application.kt 내 string 교체
HyunjeLee Oct 28, 2024
17eb041
test: 예외의 전체적인 정보 출력
HyunjeLee Oct 28, 2024
26c033b
test: 기능 테스트 3명 이상 추가
HyunjeLee Oct 28, 2024
248f8d8
test: 기능 테스트 2명 이상의 우승자
HyunjeLee Oct 28, 2024
bd7dc92
test: 기능 테스트 공백 제거
HyunjeLee Oct 28, 2024
db19f39
refactor: typo 에러로 인한 함수명 수정
HyunjeLee Oct 28, 2024
a526ae6
test: 단위 테스트 공백 제거
HyunjeLee Oct 28, 2024
88019d0
feat: 입력 횟수에 대한 예외 처리 기능 추가
HyunjeLee Oct 28, 2024
10ede80
feat: 자동차 이름 입력에 대한 예외 처리 기능 추가
HyunjeLee Oct 28, 2024
81b0953
style: 입력 횟수에 대한 예외 처리 스타일 변경
HyunjeLee Oct 28, 2024
b8f2f72
refactor(test): 기능 테스트 -> 통합 테스트로 명명 변경
HyunjeLee Oct 28, 2024
abd8407
refactor(test): 의미에 맞게끔 테스트 이름 변경
HyunjeLee Oct 28, 2024
8bee7c6
test: 예외 테스트 공백으로 이뤄진 자동차 이름 추가
HyunjeLee Oct 28, 2024
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
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,79 @@
# kotlin-racingcar-precourse

초간단 자동차 경주 게임을 구현한다.

# 구현 기능 목록

- 몇 번 이동할 것인지 횟수 입력 기능
- 자동차의 이름 입력 기능
- 쉼표(`,`) 기준으로 한 번에 n 대의 이름 입력 받기
- 해당 기능에서 자동차 갯수 확인
- 각각의 이름은 5자 이하
- 자동차의 전진 기능
- 전진 조건은 0~9 사이 무작위 값이 4 이상일 경우
- 각 실행 결과 출력 기능
- 우승자 출력 기능
- 우승자는 한 명 이상일 수 있음
- 쉼표(`,`)로 우승자 여러 명 출력
- 잘못된 입력값에 대해 IllegalArgumentException 발생 기능

# 프로그래밍 요구사항

- **`indent depth` 2 이하로 구현할 것**
- 함수는 한 가지 일만 담당하게끔 구현
- 위의 기능 목록에 대해 각각을 테스트할 것
- 외부 라이브러리 사용 불가
- `Kotlin Style Guide` 원칙

# 라이브러리
- `camp.nextstep.edu.missionutils`
- `Randoms.pickNumberInRange()`
- `Console.readLine()`

# 실행 결과 예시
```
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 횟수는 몇 회인가요?
5

실행 결과
pobi : -
woni :
jun : -

pobi : --
woni : -
jun : --

pobi : ---
woni : --
jun : ---

pobi : ----
woni : ---
jun : ----

pobi : -----
woni : ----
jun : -----

최종 우승자 : pobi, jun
```

---

# 슈도 코드 작성

```kotlin
val carMap: Map<String, Int>
val attemptNumber: Int

for (attemptNumber) {
randomDice(carMap)
printRaceResult(carMap) // carMap의 Int 이용
}

val winner = getWinner(carMap) // carMap의 Int 이용 최댓값 추출
printWinner(winner)
```
79 changes: 78 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
package racingcar

import camp.nextstep.edu.missionutils.Console.readLine
import camp.nextstep.edu.missionutils.Randoms.pickNumberInRange
import kotlin.collections.set

val isCarNameValidLength = { carName: String -> carName.length <= 5 }
val isMoveForwardValid = { pickNumberInRange(0, 9) >= 4 }
val isWinnerValid = { carMap: MutableMap<String, Int>, count: Int -> count == carMap.values.maxOrNull() }

fun main() {
Copy link

@LeeYongIn0517 LeeYongIn0517 Oct 29, 2024

Choose a reason for hiding this comment

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

리뷰 요청에 클래스 필요성에 대한 요청사항이 있어서 글 남깁니다...!
클래스는 메서드 행위의 주체, 프로퍼티 상태의 주체라서 현제님께서 작성하신 함수들이 클래스로 적절하게 묶였을 때, 그 클래스 명만 보고도 어떤 역할을하는 객체인지 쉽게 예측 가능하다는 장점이 있습니다.
클린 코드 책을 보면 코드를 작성할 때 좋은 글을 쓰려는 것처럼 하라는 말이 있더라구요...! 문장을 잘 써도 문단으로 묶는 게 중요한 것처럼, 클래스로 객체의 행위와 상태를 적절히 묶는 것도 중요하다고 합니다!

// TODO: 프로그램 구현

promptForCarNames()
val carList = processCarNames(readLine())
val carMap = initializeCarMap(carList)

promptForAttemptNumber()
val attemptNumber = processAttemptNumber(readLine())

printAllRaceResults(carMap, attemptNumber)

printWinners(carMap)
}

fun promptForCarNames() = println(Strings.MESSAGE_INPUT_CAR_NAME)
fun promptForAttemptNumber() = println(Strings.MESSAGE_INPUT_ATTEMPT_NUMBER)

fun processCarNames(input: String): List<String> {
val carList = input.split(",").map { it.trim() }
require(carList.all { it.isNotBlank() }) { Strings.MESSAGE_EXCEPTION_INPUT_CAR_NAMES }
Copy link

Choose a reason for hiding this comment

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

require로 검증하는 방법도 있군요, 하나 배워갑니다!

return carList
}

fun processAttemptNumber(input: String): Int {
require(input.toIntOrNull() != null) { Strings.MESSAGE_EXCEPTION_INPUT_ATTEMPT_NUMBER }
return input.toInt()
}


fun initializeCarMap(carList: List<String>): MutableMap<String, Int> {
val carMap = mutableMapOf<String, Int>()

for (carName in carList) {
validateCarName(carName)
carMap.put(carName, 0)
}

return carMap
}

fun printAllRaceResults(carMap: MutableMap<String, Int>, attemptNumber: Int) {
println(Strings.MESSAGE_OUTPUT_RESULT)

for (i in 0 until attemptNumber) {
printRaceResult(carMap)
println()
}
}

fun printRaceResult(carMap: MutableMap<String, Int>) {
carMap.forEach { key, value ->
moveForwardIfValid(carMap, key, value)

println("$key : " + "-".repeat(carMap[key]!!))
}
}

fun moveForwardIfValid(carMap: MutableMap<String, Int>, key: String, value: Int) {

Choose a reason for hiding this comment

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

함수명을 보면 '유효할 경우 앞으로 이동하는 작업'이라는 점을 알 수 있긴 하지만, '무엇이' 유효한지에 대해서 빠진 게 아쉽다고 생각합니다. 결국 가독성을 떨어뜨릴 수 있다고 합니다..! 만약 Car 클래스의 메서드로 moveForwardIfValid였다면 대충 차를 앞으로 보내겠구나 생각을 하겠지만, 함수로만 구현되었기 때문에 매개변수와 함수내용을 보고 로직으로 판단해야 해서 가독 비용이 높아진 것 같습니다..!

if (isMoveForwardValid()) {
Copy link

@LeeYongIn0517 LeeYongIn0517 Oct 29, 2024

Choose a reason for hiding this comment

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

isMoveForwardValid()라는 검증 메서드를 호출하고 있지만, 위로 한참 스크롤해야 하는 점에서 응집도가 떨어지는 것 같습니다. 관련성이 높으면 붙어있는 것도 좋은 방법이라고 생각합니다!

carMap[key] = value + 1
}
}

fun printWinners(carMap: MutableMap<String, Int>) = println(
Strings.MESSAGE_OUTPUT_WINNER + filterWinners(carMap).joinToString(", ")
)

fun filterWinners(carMap: MutableMap<String, Int>) = carMap.filterValues { isWinnerValid(carMap, it) }.keys

fun validateCarName(carName: String) {
require(isCarNameValidLength(carName)) { Strings.MESSAGE_EXCEPTION_CAR_NAME_LENGTH }
}
11 changes: 11 additions & 0 deletions src/main/kotlin/racingcar/Strings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racingcar

object Strings {
const val MESSAGE_INPUT_CAR_NAME = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
const val MESSAGE_INPUT_ATTEMPT_NUMBER = "시도할 횟수는 몇 회인가요?"
const val MESSAGE_OUTPUT_RESULT = "실행 결과"
const val MESSAGE_OUTPUT_WINNER = "최종 우승자 : "
const val MESSAGE_EXCEPTION_CAR_NAME_LENGTH = "자동차 이름은 5자 이하만 가능합니다."
const val MESSAGE_EXCEPTION_INPUT_CAR_NAMES = "자동차 이름은 값이 없거나 공백이 아니여야 합니다."
const val MESSAGE_EXCEPTION_INPUT_ATTEMPT_NUMBER = "입력 횟수는 정수값이여야 합니다."
}
Comment on lines +1 to +11
Copy link

Choose a reason for hiding this comment

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

상수를 String으로 분리해서 관리하는 부분 좋은 것 같습니다!

70 changes: 67 additions & 3 deletions src/test/kotlin/racingcar/ApplicationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.junit.jupiter.api.assertThrows

class ApplicationTest : NsTest() {
@Test
fun `기능 테스트`() {
fun `통합 테스트`() {
assertRandomNumberInRangeTest(
{
run("pobi,woni", "1")
Expand All @@ -20,9 +20,73 @@ class ApplicationTest : NsTest() {
}

@Test
fun `예외 테스트`() {
fun `통합 테스트 3명 이상`() {
assertRandomNumberInRangeTest(
{
run("pobi,woni,jun", "5")
assertThat(output()).contains("pobi : -", "woni : ", "jun : ", "최종 우승자 : pobi")
},
MOVING_FORWARD, STOP
)
}

@Test
fun `통합 테스트 2명 이상의 우승자`() {
assertRandomNumberInRangeTest(
{
run("pobi,woni,jun,a,b", "3")
assertThat(output()).contains(
"pobi : -",
"woni : ",
"jun : ",
"a : ",
"b : ",
"최종 우승자 : pobi, woni, jun, a, b"
)
},
MOVING_FORWARD
)
}

@Test
fun `통합 테스트 공백 제거`() {
Copy link

@LeeYongIn0517 LeeYongIn0517 Oct 29, 2024

Choose a reason for hiding this comment

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

테스트 함수명을 정할 때도 어떤 역할을 하는 함수인지 명확히 해주시면 더 좋다고 알고 있습니다. 어떤 입력인지 적은 건 좋았다고 생각합니다! 하지만 여기에 무엇을 했을 때, 어떤 결과를 기대하는지까지 적는 테스트 네이밍 룰도 있다고 하는데 개발자마다 적절한 테스트 네이밍 룰을 찾아보는 것도 좋을 거 같아요!

assertRandomNumberInRangeTest(
{
run(" a , b , c", "3")
assertThat(output()).contains(
"a : -",
"b : -",
"c : -",
"최종 우승자 : a, b, c"
)
},
MOVING_FORWARD
)
}

@Test
fun `단위 테스트 공백 제거`() {
val input = " tesla , audi , bmw "
val result = processCarNames(input)

assertThat(result).containsExactly("tesla", "audi", "bmw")
}

@Test
fun `예외 테스트 자동차 이름 5자 초과`() {
assertSimpleTest {
val exception = assertThrows<IllegalArgumentException> { runException("pobi,javaji", "1") }

exception.printStackTrace()
}
}

@Test
fun `예외 테스트 공백으로 이뤄진 자동차 이름`() {
assertSimpleTest {
assertThrows<IllegalArgumentException> { runException("pobi,javaji", "1") }
val exception = assertThrows<IllegalArgumentException> { runException(" , ,", "1") }

exception.printStackTrace()
}
}

Expand Down