diff --git a/README.md b/README.md index 1969313..5f3b371 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,80 @@ 자동차 경주 미션 저장소 +## 설정 사항 + +- (중요) JDK 17 버전으로 진행한다. + +## **기능 요구사항** + +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. + +## **실행 결과** + +- 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다. + +``` +경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분). +kokodak,kuku,cucu +시도할 회수는 몇회인가요? +5 +실행 결과 +kokodak : - +kuku : - +cucu : - +kokodak : -- +kuku : - +cucu : -- +kokodak : --- +kuku : -- +cucu : --- +kokodak : ---- +kuku : --- +cucu : ---- +kokodak : ----- +kuku : ---- +cucu : ----- +kokodak : ----- +kuku : ---- +cucu : ----- +kokodak, cucu가 최종 우승했습니다. +``` + +## **프로그래밍 요구사항** + +- **모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외** +- **자바 코드 컨벤션을 지키면서 프로그래밍한다.** + - 참고문서: https://google.github.io/styleguide/javaguide.html 또는 https://myeonguni.tistory.com/1596 +- **`규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.`**를 지키며 구현한다. + - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. +- **`규칙 2: else 예약어를 쓰지 않는다.`**를 지키며 구현한다. + - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. + - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. +- **함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.** + - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. + +**MVC 패턴으로 리팩터링 후의 main 메소드 예시** + +```java +import view.InputView; +import view.ResultView; +import domain.RacingGame; + +public class RacingMain { + public static void main(final String... args) { + final var carNames = InputView.getCarNames(); + final var tryCount = InputView.getTryCount(); + + final var racingGame = new RacingGame(carNames, tryCount); + racingGame.race(); + + ResultView.printWinners(racingGame.getWinners()); + } +} +``` \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7d7cbfc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,42 @@ +# domain +- ## Car(자동차) ## + - 이름 + - 전진 거리 + +- ## CarName(자동차 이름) ## + - 미입력 or 5글자 초과 확인 + +- ## Distance(이동 거리) ## + - 정지 or 거리 증가 + +- ## Cars(자동차 리스트) ## + - 자동차들의 정보(중복 이름 X) + - 우승자 판별 + +- ## TryConut(시도 횟수) ## + - 음수 입력 X (횟수 상한은 없나??) + +# ~~dto~~ + +# controller +- 이름 입력받아서 Cars로 저장 +- 시도 횟수 입력받아 TryCount로 저장 +- 레이스 진행 +- 결과 출력 + +# utils +- ## Parser ## + - 이름: 쉼표 기준으로 분리 + - 시도 횟수 + +- ## RandomNumberGenerator ## + - 0~9 랜덤 숫자 생성기 + +# view +- ## InputView ## + - 자동차 이름 입력 + - 시도 횟수 입력 + +- ## OutView ## + - 실행 결과 출력 + - 우승자 출력 \ No newline at end of file diff --git a/src/main/java/RacingMain.java b/src/main/java/RacingMain.java index 4394287..dcad986 100644 --- a/src/main/java/RacingMain.java +++ b/src/main/java/RacingMain.java @@ -1,7 +1,10 @@ +import controller.RacingCarController; + public class RacingMain { public static void main(String[] args) { // TODO: MVC 패턴을 기반으로 자동차 경주 미션 구현해보기 - System.out.println("Hello, World!"); + RacingCarController racingCarController = new RacingCarController(); + racingCarController.run(); } } diff --git a/src/main/java/controller/RacingCarController.java b/src/main/java/controller/RacingCarController.java new file mode 100644 index 0000000..58518de --- /dev/null +++ b/src/main/java/controller/RacingCarController.java @@ -0,0 +1,66 @@ +package controller; + +import domain.Car; +import domain.Cars; +import domain.TryCount; +import utils.RandomNumberGenerator; +import view.InputView; +import view.OutputView; + +import java.io.IOException; +import java.util.List; + +public class RacingCarController { + + private final RandomNumberGenerator randomNumberGenerator; + + public RacingCarController() { + this.randomNumberGenerator = new RandomNumberGenerator(); + } + + public void run() { + Cars cars = getCars(); + TryCount tryCount = getTryCount(); + race(cars, tryCount); + printWinner(cars); + } + + private Cars getCars() { + List carNames = InputView.readCarNames(); + try { + return new Cars(carNames); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return getCars(); //올바른 입력 넣을 때까지 반복 + } + } + + private TryCount getTryCount() { + try { + int number = InputView.readTryCount(); + return new TryCount(number); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return getTryCount(); //올바른 입력 넣을 때까지 반복 + } + } + + private void race(Cars cars, TryCount tryCount) { + OutputView.printResult(); + while (tryCount.isRemain()) { + cars.moveCars(randomNumberGenerator); + printStatus(cars); + tryCount.decrease(); + } + } + + private void printStatus(Cars cars) { + List carList = cars.getCars(); + OutputView.printStatus(carList); + } + + private void printWinner(Cars cars) { + List winnerList = cars.findWinner(); + OutputView.printWinners(winnerList); + } +} diff --git a/src/main/java/domain/Car.java b/src/main/java/domain/Car.java new file mode 100644 index 0000000..549e875 --- /dev/null +++ b/src/main/java/domain/Car.java @@ -0,0 +1,28 @@ +package domain; + +public class Car { + + private static final int CAN_MOVE_NUMBER = 4; + private final CarName carName; + private final Distance distance; + + + public Car(String carName) { + this.carName = new CarName(carName); + this.distance = new Distance(); + } + + public void move(int number) { + if (number >= CAN_MOVE_NUMBER) { + distance.increaseDistance(); + } + } + + public String getCarName() { + return carName.getCarName(); + } + + public int getDistance() { + return distance.getDistance(); + } +} diff --git a/src/main/java/domain/CarName.java b/src/main/java/domain/CarName.java new file mode 100644 index 0000000..1eed06b --- /dev/null +++ b/src/main/java/domain/CarName.java @@ -0,0 +1,23 @@ +package domain; + +public class CarName { + private final String carName; + + public CarName(String carName) { + validateCarName(carName); + this.carName = carName; + } + + public String getCarName() { + return carName; + } + + private void validateCarName(String carName) { //이름 글자수 확인(없거나, 5글자 초과일 시) + if (carName.isEmpty()) { + throw new IllegalArgumentException("이름이 반드시 존재해야 합니다."); + } + if (carName.length() > 5) { + throw new IllegalArgumentException("이름은 5글자 이하여야 합니다."); + } + } +} diff --git a/src/main/java/domain/Cars.java b/src/main/java/domain/Cars.java new file mode 100644 index 0000000..16a55ba --- /dev/null +++ b/src/main/java/domain/Cars.java @@ -0,0 +1,62 @@ +package domain; + +import utils.RandomNumberGenerator; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class Cars { + + private static final String DUPLICATED_NAMES_MASSAGE= "자동차의 이름은 중복될 수 없습니다."; + private static final int DEFAULT_DISTANCE = 0; + private final List cars; + + public Cars(List carNames) { + final List cars = generateCars(carNames); + validateDuplication(carNames); + this.cars = cars; + } + + private List generateCars(List cars) { + return cars.stream() + .map(Car::new) + .collect(Collectors.toList()); + } + + public void moveCars(RandomNumberGenerator randomNumberGenerator) { + for (Car car : cars) { + int randomNumber = randomNumberGenerator.generate(); + car.move(randomNumber); + } + } + +// 이름 중복 확인 + private void validateDuplication(List carNames) { + Set duplicationCheck = new HashSet<>(carNames); + if (carNames.size() != duplicationCheck.size()) { + throw new IllegalArgumentException(DUPLICATED_NAMES_MASSAGE); + } + } + +// 최대 이동 거리 구하기 + private int findMaxDistance() { + return cars.stream() + .mapToInt(Car::getDistance) + .max() + .orElse(DEFAULT_DISTANCE); + } + +// 최대 이동 거리와 같은 위치에 있는 자동차들 찾기 + public List findWinner() { + int maxDistance = findMaxDistance(); + return cars.stream() + .filter(car -> car.getDistance() == maxDistance) + .collect(Collectors.toList()); + } + + public List getCars() { + return cars; + } +} diff --git a/src/main/java/domain/Distance.java b/src/main/java/domain/Distance.java new file mode 100644 index 0000000..abbed75 --- /dev/null +++ b/src/main/java/domain/Distance.java @@ -0,0 +1,19 @@ +package domain; + +public class Distance { + + private static final int INITIAL_VALUE = 0; + private int distance; + + public Distance() { + this.distance = INITIAL_VALUE; + } + + public void increaseDistance() { + distance++; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/domain/TryCount.java b/src/main/java/domain/TryCount.java new file mode 100644 index 0000000..efadbae --- /dev/null +++ b/src/main/java/domain/TryCount.java @@ -0,0 +1,32 @@ +package domain; + +public class TryCount { + + private static final String NOT_POSITIVE_INTEGER_MESSAGE = "시도 횟수는 양의 정수여야 합니다"; + private static final int MINIMUM_COUNT = 1; + + private int tryCount; + + public TryCount(int tryCount) { + validate(tryCount); + this.tryCount = tryCount; + } + + private void validate(int tryCount) { + if (tryCount <= 0) { + throw new IllegalArgumentException(NOT_POSITIVE_INTEGER_MESSAGE); + } + } + + public void decrease() { + tryCount--; + } + + public boolean isRemain() { + return tryCount >= MINIMUM_COUNT; + } + + public int getTryCount() { + return tryCount; + } +} diff --git a/src/main/java/utils/Parser.java b/src/main/java/utils/Parser.java new file mode 100644 index 0000000..d65493b --- /dev/null +++ b/src/main/java/utils/Parser.java @@ -0,0 +1,23 @@ +package utils; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Parser { + + private static final String NOT_POSITIVE_INTEGER = "시도 횟수는 정수여야 합니다"; + public static List parseNames(String input) { + return Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + } + + public static int parseCount(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(NOT_POSITIVE_INTEGER); + } + } +} diff --git a/src/main/java/utils/RandomNumberGenerator.java b/src/main/java/utils/RandomNumberGenerator.java new file mode 100644 index 0000000..ae2349b --- /dev/null +++ b/src/main/java/utils/RandomNumberGenerator.java @@ -0,0 +1,7 @@ +package utils; + +public class RandomNumberGenerator { + public int generate() { + return (int) (Math.random()*10); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000..dc05ae6 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,29 @@ +package view; + +import utils.Parser; + +import java.util.List; +import java.util.Scanner; + +public class InputView { + private static final String READ_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."; + private static final String READ_TRY_COUNT_MESSAGE = "시도할 횟수는 몇회인가요?"; + + private static Scanner scanner; + + public InputView(Scanner scanner) { + InputView.scanner = scanner; + } + + public static List readCarNames() { + System.out.println(READ_CAR_NAMES_MESSAGE); + String input = scanner.nextLine(); + return Parser.parseNames(input); + } + + public static int readTryCount() { + System.out.println(READ_TRY_COUNT_MESSAGE); + String input = scanner.nextLine(); + return Parser.parseCount(input); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 0000000..1cdc040 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,39 @@ +package view; + +import domain.Car; +import domain.Cars; + +import java.util.List; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String RESULT_MESSAGE = "실행 결과"; + private static final String STATUS_FORMAT = "%s : %s\n"; + private static final String WINNER_FORMAT = "%s가 최종 우승했습니다."; + private static final String HYPHEN = "-"; + + public static void printResult() { + System.out.println(RESULT_MESSAGE); + } + + public static void printStatus(List cars) { + for (Car car : cars) { + String currentDistance = getCurrentDistance(car.getDistance()); + System.out.printf(STATUS_FORMAT, car.getCarName(), currentDistance); + } + System.out.println(); + } + + public static void printWinners(List cars) { + List carNames = cars.stream() + .map(Car::getCarName) + .toList(); + String winners = String.join(",", carNames); + System.out.printf(WINNER_FORMAT, winners); + } + + private static String getCurrentDistance(int distance) { + return HYPHEN.repeat(distance); + } +} diff --git a/src/test/java/domain/CarNameTest.java b/src/test/java/domain/CarNameTest.java new file mode 100644 index 0000000..9e9debf --- /dev/null +++ b/src/test/java/domain/CarNameTest.java @@ -0,0 +1,40 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatCode; + +public class CarNameTest { + + @DisplayName("이름이 없거나, 5글자를 초과하면 예외") + @ParameterizedTest + @CsvSource(value = {"'', 이름이 반드시 존재해야 합니다.", + "asdfgh, 이름은 5글자 이하여야 합니다.", + "aassddff, 이름은 5글자 이하여야 합니다."}) + void 이름_예외_발생(String carName, String errorMessage) { + assertThatThrownBy(() -> new CarName(carName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(errorMessage); + } + + @DisplayName("1~5글자 이름은 유효") + @ParameterizedTest + @CsvSource(value = {"asdf", "ff", "jklkj"}) + void 유효한_이름(String carName) { + assertThatCode(() -> new CarName(carName)) + .doesNotThrowAnyException(); + } + + +// @ParameterizedTest //하나의 테스트 메소드로 여러 개의 파라미터에 대해서 테스트할 수 있다고 함 +// @ValueSource //리터럴 값의 단일 배열을 지정할 수 있음 +// @CsvSource //입력값에 따라 결과값이 다른 경우를 테스트 하려면 //CSV(Comma Separated Values) + +// assertThatThrownBy: 예외가 발생하는 것을 검증할 때 사용 +// assertThatCode: 예외가 발생하지 않는 것을 검증할 때 사용 +} diff --git a/src/test/java/domain/CarTest.java b/src/test/java/domain/CarTest.java new file mode 100644 index 0000000..e6f0825 --- /dev/null +++ b/src/test/java/domain/CarTest.java @@ -0,0 +1,29 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; + +class CarTest { + + @DisplayName("0~3일 때는 움직이지 않음") + @ParameterizedTest + @CsvSource(value = {"0,0", "1,0", "2,0", "3,0"}) //생성했을 때의 distance의 INITIAL_VALUE = 0 + void 움직이지_않음(int number, int distance) { + Car car = new Car("test"); + car.move(number); + assertThat(car.getDistance()).isEqualTo(distance); + } + + @DisplayName("4~9일 때는 움직임") + @ParameterizedTest + @CsvSource(value = {"4,1", "5,1", "6,1", "7,1", "8,1", "9,1"}) //생성했을 때의 distance의 INITIAL_VALUE = 0 + void 움직임(int number, int distance) { + Car car = new Car("test"); + car.move(number); + assertThat(car.getDistance()).isEqualTo(distance); + } +} \ No newline at end of file diff --git a/src/test/java/domain/CarsTest.java b/src/test/java/domain/CarsTest.java new file mode 100644 index 0000000..9b31b46 --- /dev/null +++ b/src/test/java/domain/CarsTest.java @@ -0,0 +1,13 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; + +class CarsTest { + +} \ No newline at end of file diff --git a/src/test/java/domain/DistanceTest.java b/src/test/java/domain/DistanceTest.java new file mode 100644 index 0000000..46b0ed7 --- /dev/null +++ b/src/test/java/domain/DistanceTest.java @@ -0,0 +1,20 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; + +class DistanceTest { + + @DisplayName("increase는 distance를 1 증가시킴") + @ParameterizedTest + @CsvSource(value = {"1"}) + void 이동거리_증가(int afterValue) { + Distance distance = new Distance(); //애초에 기본 세팅이 0 + distance.increaseDistance(); + assertThat(distance.getDistance()).isEqualTo(afterValue); + } +} \ No newline at end of file diff --git a/src/test/java/domain/TryCountTest.java b/src/test/java/domain/TryCountTest.java new file mode 100644 index 0000000..542fa8e --- /dev/null +++ b/src/test/java/domain/TryCountTest.java @@ -0,0 +1,46 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; + +class TryCountTest { + + @DisplayName("시도 횟수가 0이거나 음수이면 예외") + @ParameterizedTest + @CsvSource(value = {"-1", "-12", "0"}) + void 양의_정수가_아님(int tryCount) { + assertThatThrownBy(() -> new TryCount(tryCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도 횟수는 양의 정수여야 합니다"); + } + + @DisplayName("시도 횟수가 양수이면 유효") + @ParameterizedTest + @CsvSource(value = {"1", "5", "48"}) + void 유효한_시도_횟수(int tryCount) { + assertThatCode(() -> new TryCount(tryCount)) + .doesNotThrowAnyException(); + } + + @DisplayName("decrease는 tryCount를 1 감소시킴") + @ParameterizedTest + @CsvSource(value = {"1, 0", "5, 4", "48, 47"}) + void 시도_횟수_감소(int beforeValue, int afterValue) { + TryCount tryCount = new TryCount(beforeValue); + tryCount.decrease(); + assertThat(tryCount.getTryCount()).isEqualTo(afterValue); + } + + @DisplayName("isRemain은 시도 횟수가 남았는지 확인함") + @ParameterizedTest + @CsvSource(value = {"1, false", "5, true"}) + void 시도_횟수_남았는지_확인(int beforeValue, boolean isRemain) { + TryCount tryCount = new TryCount(beforeValue); + tryCount.decrease(); + assertThat(tryCount.isRemain()).isEqualTo(isRemain); + } +} \ No newline at end of file diff --git a/src/test/java/utils/ParserTest.java b/src/test/java/utils/ParserTest.java new file mode 100644 index 0000000..52c0b9d --- /dev/null +++ b/src/test/java/utils/ParserTest.java @@ -0,0 +1,33 @@ +package utils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +class ParserTest { + + @DisplayName("쉼표를 기준으로 이름 분리") +// @ParameterizedTest +// @CsvSource(value = {"aa,bb,cc", "aaa, bbb,ccc ", "a,,b,c"}) + @Test + void 이름_분리() { + String string = "aa,bb,cc"; + List names = Parser.parseNames(string); + assertThat(names).containsExactly("aa", "bb", "cc"); + } + + @DisplayName("쉼표가 연속으로 오면 빈 문자열로 처리") + @Test + void 쉼표_연속() { + String string = "a,,b,c"; + List names = Parser.parseNames(string); + assertThat(names).containsExactly("a", "", "b", "c"); + } +} \ No newline at end of file