Skip to content
213 changes: 86 additions & 127 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,163 +1,122 @@
# 미션 - 자동차 경주

## 🔍 진행 방식

- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

## 📮 미션 제출 방법

- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다.
- GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고해 제출한다.
- GitHub에 미션을 제출한 후 [우아한테크코스 지원](https://apply.techcourse.co.kr) 사이트에 접속하여 프리코스 과제를 제출한다.
- 자세한 방법은 [제출 가이드](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 참고
- **Pull Request만 보내고 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.**

## 🚨 과제 제출 전 체크 리스트 - 0점 방지

- 기능 구현을 모두 정상적으로 했더라도 **요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리**한다.
- 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는지 확인한다.
- **테스트가 실패할 경우 0점으로 처리**되므로, 반드시 확인 후 제출한다.

### 테스트 실행 가이드

- 터미널에서 `java -version`을 실행하여 Java 버전이 17인지 확인한다.
Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 17로 실행되는지 확인한다.
- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고,
Windows 사용자의 경우 `gradlew.bat clean test` 또는 `./gradlew.bat clean test` 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다.

```
BUILD SUCCESSFUL in 0s
```
### 기능구현 목록

- [x] 경주할 자동차들의 이름과 시도할 횟수를 입력받는다
- [x] 유효성 검사와 예외 처리를 통해 올바른 입력만 허용한다. (예: 빈 입력, 중복된 이름, 자동차 이름 길이 등)
- [x] 회차별로 자동차를 전진시킨 후 모든 회차가 끝난 후 우승한 자동차를 선정한다
- [x] 회차별로 각 자동차 별로 0~9사이의 무작위 값을 생성한다.
- [x] 무작위 값 생성 시 테스트를 위해 난수 생성기를 주입할 수 있도록 설계한다.
- [x] 자동차 이동 여부 판정:
- [x] 생성된 값이 4 이상이면 1칸 이동
- [x] 생성된 값이 4 미만이면 변화 없음
- [x] 회차별로 이동한 횟수만큼 "-"를 통해 이동 상태를 표시한다.
- [x] 모든 회차가 끝난 후 가장 많이 이동한 자동차를 우승자로 선정한다.
- [x] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분

**예외처리**

`IllegalArgumentException` 이후 프로그램이 종료되도록 한다.

- [x] **자동차 이름 입력 예외처리**
- [x] 자동차 이름 길이가 5자를 넘는 경우
- [x] 빈 이름이 포함된 경우의 자동차 이름
- [x] 같은 이름이 있는 경우 중복 처리
- [x] **시도할 횟수 예외처리**
- [x] 숫자가 아닌 경우에 대한 처리
- [x] 음수 또는 0일 경우에 대한 처리
- [x] 빈 입력에 대한 처리

---
# 이펙티브 스터디

## 🚀 기능 요구 사항

초간단 자동차 경주 게임을 구현한다.
## 아이템 17. 변경 가능성을 최소화하라

- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.
불변 클래스란?

### 입출력 요구 사항
- 그 인스턴스의 내부 값을 수정할 수 없는 클래스.
불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되 전까지 절대객체릐 달라지지 않음

#### 입력
### **클래스를 불변으로 만드려면**

- 경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분)
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다
- 클래스를 확장할 수 없도록 한다
- 모든 필드를 final로 선언한다
- 모든 필드를 private로 선언 한다
- 자신 외에는 내부의 가변 컴포넌트에 접근 할 수 없도록 한다

```
pobi,woni,jun
```
정적 팩터리를 사용한 불변 클래스

- 시도할 회수

```
5
```java
public final class Money {
private final int amount;
private final String currency;

// 생성자는 private → 외부에서 new로 만들 수 없음
private Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}

// 정적 팩토리 메서드 (이름 있는 생성자 역할)
public static Money of(int amount, String currency) {
return new Money(amount, currency);
}
}
```

#### 출력
### 코드에서의 활용

- 각 차수별 실행 결과
**TryCount를 불변 클래스로 사용**

```
pobi : --
woni : ----
jun : ---
```
- 객체에서 변경되어서는 안되는 필드를 private, final 사용함

- 단독 우승자 안내 문구
Cars는 일급 컬렉션으로 컬렉션 자체는 불변이지만 내부 요소는 가변

```
최종 우승자 : pobi
```
- Car의 경우는 움직여야 하기 때문

- 공동 우승자 안내 문구
생성자가 하나뿐이기에 정적 팩터리메서드 사용 X

```
최종 우승자 : pobi, jun
```
## 아이템18. 상속보다는 컴포지션을 사용하라

#### 실행 결과 예시
### 컴포지션 이란?

```
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 회수는 몇회인가요?
5
어떤 객체가 다른 객체를 포함해서 사용하는것(has-a 관계)

실행 결과
pobi : -
woni :
jun : -
어떤 객체가 다른 객체의 기능 을 사용하고 싶을때 그 객체를 자신의 필드로 가지고 있다가 필요한 시점에 위임 하는 방식

pobi : --
woni : -
jun : --
상속**(inheritance)은 강력하지만, 잘못 사용하면 결합도(coupling)가 높아지고, 유연성 떨어지며, 오류를 유발가능**

pobi : ---
woni : --
jun : ---
부모클래스의 내부 구현에 강하게 의존하므로, 부모클래스가 바뀌면 자식클래스도 영향을 크게 받음

pobi : ----
woni : ---
jun : ----
**컴포지션(composition)은 필드로 다른 객체를 가지고 있으며, 그 객체에 기능을 위임함으로 더 유연하고 안정적 설계가 가능**

pobi : -----
woni : ----
jun : -----
### 코드에서의 활용

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

---
- 자동차 전진 조건을 전략 패턴을 이용함
- **Car가 직접 이동 조건을 판단하지 않고, 전략 객체에게 위임**
- 필드로 가지고 있지않지만 이걸 컴포지션이라고 볼 수 있는지가 궁금

## 🎯 프로그래밍 요구 사항
## 아이템 10, 11 equals 와 hashcode

- JDK 17 버전에서 실행 가능해야 한다. **JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.**
- 프로그램 실행의 시작점은 `Application`의 `main()`이다.
- `build.gradle` 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
- [Java 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/master/styleguide/java) 가이드를 준수하며 프로그래밍한다.
- 프로그램 종료 시 `System.exit()`를 호출하지 않는다.
- 프로그램 구현이 완료되면 `ApplicationTest`의 모든 테스트가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.**
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
**equals(Object obj)**

### 추가된 요구 사항
- 두 객체가 논리적으로 같은지 비교
- Object 기본 구현은 단순히 주소비교 (==)
- 필요시 오버라이딩 해서 본인이 정의한 동등성 기준 넣어야함

- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
- 테스트 도구 사용법이 익숙하지 않다면 `test/java/study`를 참고하여 학습한 후 테스트를 구현한다.
**hashCode()**

### 라이브러리
- 객체를 식별하는 **정수 해시값** 반환
- HashSet, HashMap 같은 **해시 기반 자료구조**에서 필수
- equals()가 true라면 **hashCode도 반드시 같아야 함**

- JDK에서 제공하는 Random 및 Scanner API 대신 `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다.
- Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다.
- 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.
### **equals 재정의 → 반드시 hashCode도 재정의**

#### 사용 예시
- 객체가 논리적으로 같은지 비교시 먼저 **hashCode 비교후 equals 호출**

- 0에서 9까지의 정수 중 한 개의 정수 반환
### 코드에서의 활용

```java
Randoms.pickNumberInRange(0,9);
```

---
- 자동차 이름이 같으면 안된다. 이것을 객체 단에서 처리

## ✏️ 과제 진행 요구 사항
equals(Object obj) → 객체가 같다는 기준 정의 (역기서는 자동차의 이름)

- 미션은 [java-racingcar-6](https://github.com/woowacourse-precourse/java-racingcar-6) 저장소를 Fork & Clone해 시작한다.
- **기능을 구현하기 전 `docs/README.md`에 구현할 기능 목록을 정리**해 추가한다.
- **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다.
- [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다.
- 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고한다.
hashCode() → `Set<Car> uniqueCars = new HashSet<>(carList);` 으로 중복검사 위해서
8 changes: 7 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package racingcar;

import racingcar.config.AppConfig;
import racingcar.controller.RacingGameController;
import racingcar.domain.strategy.RandomMoveStrategy;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
AppConfig config = new AppConfig();
RacingGameController controller = config.racingGameController();
controller.run(new RandomMoveStrategy());
}
}
26 changes: 26 additions & 0 deletions src/main/java/racingcar/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package racingcar.config;


import racingcar.controller.RacingGameController;
import racingcar.service.RacingGameService;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public class AppConfig {

public RacingGameController racingGameController() {
return new RacingGameController(inputView(), outputView(), racingGameService());
}

private InputView inputView() {
return new InputView();
}

private OutputView outputView() {
return new OutputView();
}

private RacingGameService racingGameService() {
return new RacingGameService();
}
}
43 changes: 43 additions & 0 deletions src/main/java/racingcar/controller/RacingGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package racingcar.controller;

import racingcar.domain.Cars;
import racingcar.domain.TryCount;
import racingcar.domain.strategy.MoveStrategy;
import racingcar.service.RacingGameService;
import racingcar.view.InputView;
import racingcar.view.OutputView;
import racingcar.util.InputParser;

import java.util.Arrays;
import java.util.List;

public class RacingGameController {
private final InputView inputView;
private final OutputView outputView;
private final RacingGameService racingGameService;

public RacingGameController(InputView inputView, OutputView outputView, RacingGameService racingGameService) {
this.inputView = inputView;
this.outputView = outputView;
this.racingGameService = racingGameService;
}
public void run(MoveStrategy strategy) {
Cars cars = createRacingCars();
TryCount tryCount = new TryCount(InputParser.parseTryCount(inputView.readTryCount()));
repeatMoveAndPrint(cars, tryCount.getValue(), strategy);
outputView.printWinners(cars.findWinners());

}

private void repeatMoveAndPrint(Cars cars, int tryCount, MoveStrategy strategy) {
for (int i = 0; i < tryCount; i++) {
cars.moveAll(strategy);
outputView.printCars(cars.getCars());
}
}

private Cars createRacingCars() {
List<String> carNames = InputParser.parseCarNames(inputView.createCarNames());
return racingGameService.createCars(carNames);
}
}
54 changes: 54 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package racingcar.domain;

import racingcar.domain.strategy.MoveStrategy;
import racingcar.util.ErrorMessages;

import java.util.Objects;

public class Car {
private static final int START_POSITION = 0;
private static final int MAX_CAR_NAME_LENGTH = 5;

private final String carName;
private int position = START_POSITION;

public Car(String carName) {
validateCarName(carName);
this.carName = carName;
}

private void validateCarName(String carName) {
if (carName == null || carName.isBlank()) {
throw new IllegalArgumentException(ErrorMessages.INVALID_CAR_NAME_BLANK);
}
if (carName.length() > MAX_CAR_NAME_LENGTH) {
throw new IllegalArgumentException(ErrorMessages.TOO_LONG_CAR_NAME);
}
}
public void move(MoveStrategy strategy) {
if (strategy.movable()) {
position++;
}
}

public String getName() {
return carName;
}

public int getPosition() {
return position;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Car)) return false;
Car car = (Car) o;
return Objects.equals(carName, car.carName);
}

@Override
public int hashCode() {
return Objects.hash(carName);
}
}
Loading