diff --git a/build.gradle b/build.gradle index 239f9e78..752ce060 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,9 @@ dependencies { testImplementation platform('org.assertj:assertj-bom:3.25.1') testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('org.assertj:assertj-core') + + testImplementation ('org.mockito:mockito-core:5.2.0') + testImplementation ('org.mockito:mockito-junit-jupiter:5.2.0') } test { diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..ed8bc373 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,17 @@ +import domain.Game; +import util.RandomNumberProvider; +import view.InputView; +import view.ResultView; + +public class Application { + + public static void main(final String... args) { + final String carNames = InputView.inputCarNames(); + final int tryCount = InputView.inputTryCount(); + + final Game game = new Game(carNames, tryCount, new RandomNumberProvider()); + game.play(); + + ResultView.outputResult(game); + } +} diff --git a/src/main/java/domain/Car.java b/src/main/java/domain/Car.java new file mode 100644 index 00000000..d3b3fc6a --- /dev/null +++ b/src/main/java/domain/Car.java @@ -0,0 +1,46 @@ +package domain; + +import exception.CarNameRequiredException; +import exception.CarNameTooLongException; +import util.NumberProvider; + +public class Car { + + private static final int MOVE_CONDITION = 4; + private static final int MAX_NAME_LENGTH = 5; + private final String name; + private int position = 0; + + public Car(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (name == null || name.isEmpty()) { + throw new CarNameRequiredException("자동차 이름은 필수입니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new CarNameTooLongException("자동차 이름은 5자 이하만 가능합니다."); + } + } + + public void move(NumberProvider numberProvider) { + int randomNumber = numberProvider.provide(); + if (isMovable(randomNumber)) { + position++; + } + } + + private boolean isMovable(int randomValue) { + return randomValue >= MOVE_CONDITION; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/domain/CarGroup.java b/src/main/java/domain/CarGroup.java new file mode 100644 index 00000000..702eb61f --- /dev/null +++ b/src/main/java/domain/CarGroup.java @@ -0,0 +1,39 @@ +package domain; + +import java.util.List; +import java.util.stream.Collectors; + +import util.NumberProvider; + +public class CarGroup { + + private final List cars; + private final NumberProvider numberProvider; + + public CarGroup(List cars, NumberProvider numberProvider) { + this.cars = cars; + this.numberProvider = numberProvider; + } + + public void moveCars() { + cars.forEach(car -> car.move(numberProvider)); + } + + public List getFarthestCars() { + int maxPosition = getMaxPosition(); + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .collect(Collectors.toList()); + } + + private int getMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } + + public List getCars() { + return cars; + } +} diff --git a/src/main/java/domain/Game.java b/src/main/java/domain/Game.java new file mode 100644 index 00000000..70525e80 --- /dev/null +++ b/src/main/java/domain/Game.java @@ -0,0 +1,41 @@ +package domain; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import exception.TryCountOutOfRangeException; +import factory.CarFactory; +import util.NumberProvider; + +public class Game { + + private final CarGroup carGroup; + private final int tryCount; + + public Game(String carNames, int tryCount, NumberProvider numberProvider) { + validateTryCountIsPositive(tryCount); + this.carGroup = new CarGroup(CarFactory.createCars(carNames), numberProvider); + this.tryCount = tryCount; + } + + private void validateTryCountIsPositive(int tryCount) { + if (tryCount < 1) { + throw new TryCountOutOfRangeException("라운드는 1 이상이어야 합니다."); + } + } + + public void play() { + for (int i = 0; i < tryCount; i++) { + carGroup.moveCars(); + } + } + + public List getWinners() { + return carGroup.getFarthestCars(); + } + + public CarGroup getCarGroup() { + return carGroup; + } +} diff --git a/src/main/java/exception/CarNameRequiredException.java b/src/main/java/exception/CarNameRequiredException.java new file mode 100644 index 00000000..bdd484b2 --- /dev/null +++ b/src/main/java/exception/CarNameRequiredException.java @@ -0,0 +1,8 @@ +package exception; + +public class CarNameRequiredException extends IllegalArgumentException { + + public CarNameRequiredException(String message) { + super(message); + } +} diff --git a/src/main/java/exception/CarNameTooLongException.java b/src/main/java/exception/CarNameTooLongException.java new file mode 100644 index 00000000..a8690c03 --- /dev/null +++ b/src/main/java/exception/CarNameTooLongException.java @@ -0,0 +1,8 @@ +package exception; + +public class CarNameTooLongException extends IllegalArgumentException { + + public CarNameTooLongException(String message) { + super(message); + } +} diff --git a/src/main/java/exception/TryCountOutOfRangeException.java b/src/main/java/exception/TryCountOutOfRangeException.java new file mode 100644 index 00000000..378535e6 --- /dev/null +++ b/src/main/java/exception/TryCountOutOfRangeException.java @@ -0,0 +1,8 @@ +package exception; + +public class TryCountOutOfRangeException extends IllegalArgumentException { + + public TryCountOutOfRangeException(String message) { + super(message); + } +} diff --git a/src/main/java/factory/CarFactory.java b/src/main/java/factory/CarFactory.java new file mode 100644 index 00000000..ea02bf3d --- /dev/null +++ b/src/main/java/factory/CarFactory.java @@ -0,0 +1,15 @@ +package factory; + +import domain.Car; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class CarFactory { + + public static List createCars(String carNames) { + return Arrays.stream(carNames.split(",")) + .map(Car::new) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/util/NumberProvider.java b/src/main/java/util/NumberProvider.java new file mode 100644 index 00000000..9c717c98 --- /dev/null +++ b/src/main/java/util/NumberProvider.java @@ -0,0 +1,6 @@ +package util; + +public interface NumberProvider { + + int provide(); +} diff --git a/src/main/java/util/RandomNumberProvider.java b/src/main/java/util/RandomNumberProvider.java new file mode 100644 index 00000000..00762330 --- /dev/null +++ b/src/main/java/util/RandomNumberProvider.java @@ -0,0 +1,14 @@ +package util; + +import java.util.Random; + +public class RandomNumberProvider implements NumberProvider { + + private static final int BOUND = 10; + private final Random random = new Random(); + + @Override + public int provide() { + return random.nextInt(BOUND); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..6d4717a5 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,18 @@ +package view; + +import java.util.Scanner; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + + public static String inputCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + return scanner.nextLine(); + } + + public static int inputTryCount() { + System.out.println("시도할 회수는 몇회인가요?"); + return scanner.nextInt(); + } +} diff --git a/src/main/java/view/ResultView.java b/src/main/java/view/ResultView.java new file mode 100644 index 00000000..687d1008 --- /dev/null +++ b/src/main/java/view/ResultView.java @@ -0,0 +1,34 @@ +package view; + +import java.util.List; + +import domain.Car; +import domain.Game; + +public class ResultView { + + public static void outputResult(Game game) { + outputCarPosition(game.getCarGroup().getCars()); + outputWinner(game.getWinners()); + } + + private static void outputCarPosition(List cars) { + System.out.println(); + System.out.println("실행결과"); + for (Car car : cars) { + System.out.printf("%s : %s%n", car.getName(), "-".repeat(car.getPosition())); + } + System.out.println(); + } + + private static void outputWinner(List winnerCars) { + List winnerNames = getWinnerCarNames(winnerCars); + System.out.printf("%s가 최종 우승했습니다.", String.join(", ", winnerNames)); + } + + private static List getWinnerCarNames(List winners) { + return winners.stream() + .map(Car::getName) + .toList(); + } +} diff --git a/src/test/java/domain/CarGroupTest.java b/src/test/java/domain/CarGroupTest.java new file mode 100644 index 00000000..c0abbb40 --- /dev/null +++ b/src/test/java/domain/CarGroupTest.java @@ -0,0 +1,39 @@ +package domain; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import fixture.FixedNumberProvider; + +public class CarGroupTest { + + @Test + @DisplayName("positive - 자동차 그룹이 올바르게 이동한다.") + void moveCars() { + List cars = List.of(new Car("차"), new Car("차차"), new Car("차차차")); + CarGroup carGroup = new CarGroup(cars, new FixedNumberProvider.Number4Provider()); + carGroup.moveCars(); + assertThat(cars).allMatch(car -> car.getPosition() == 1); + } + + @Test + @DisplayName("positive - 가장 멀리 간 자동차를 반환한다.") + void getFarthestCars() { + Car car1 = new Car("A"); + Car car2 = new Car("B"); + Car car3 = new Car("C"); + car1.move(new FixedNumberProvider.Number4Provider()); // 1칸 이동 + car2.move(new FixedNumberProvider.Number4Provider()); // 1칸 이동 + car3.move(new FixedNumberProvider.Number3Provider()); // 이동 X + + List cars = List.of(car1, car2, car3); + CarGroup carGroup = new CarGroup(cars, new FixedNumberProvider.Number4Provider()); + List farthestCars = carGroup.getFarthestCars(); + + assertThat(farthestCars).containsExactlyInAnyOrder(car1, car2); + } +} diff --git a/src/test/java/domain/CarTest.java b/src/test/java/domain/CarTest.java new file mode 100644 index 00000000..d47b9485 --- /dev/null +++ b/src/test/java/domain/CarTest.java @@ -0,0 +1,35 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import fixture.FixedNumberProvider; + +class CarTest { + + @Test + @DisplayName("positive - 4 이상의 수일 경우 자동차가 움직인다.") + void moveCar() { + Car car = new Car("차"); + car.move(new FixedNumberProvider.Number4Provider()); + assertThat(car.getPosition()).isEqualTo(1); + } + + @Test + @DisplayName("positive - 4 보다 작은 수일 경우 자동차가 움직이지 않는다.") + void doNotMoveCar() { + Car car = new Car("차차"); + car.move(new FixedNumberProvider.Number3Provider()); + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + @DisplayName("negative - 이름이 null 혹은 Empty 경우 예외처리") + void nameException() { + assertThrows(IllegalArgumentException.class, () -> new Car(null)); + assertThrows(IllegalArgumentException.class, () -> new Car("")); + } +} diff --git a/src/test/java/domain/GameTest.java b/src/test/java/domain/GameTest.java new file mode 100644 index 00000000..be97c357 --- /dev/null +++ b/src/test/java/domain/GameTest.java @@ -0,0 +1,19 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import util.RandomNumberProvider; + +class GameTest { + + @Test + @DisplayName("negative - 라운드가 0 이하일 경우 예외 발생") + void tryCountIsPositive() { + String carNames = "차1,차2,차3"; + assertThrows(IllegalArgumentException.class, () -> new Game(carNames, 0, new RandomNumberProvider())); + assertThrows(IllegalArgumentException.class, () -> new Game(carNames, -1, new RandomNumberProvider())); + } +} diff --git a/src/test/java/fixture/FixedNumberProvider.java b/src/test/java/fixture/FixedNumberProvider.java new file mode 100644 index 00000000..733ab8fb --- /dev/null +++ b/src/test/java/fixture/FixedNumberProvider.java @@ -0,0 +1,20 @@ +package fixture; + +import util.NumberProvider; + +public class FixedNumberProvider { + + public static class Number3Provider implements NumberProvider { + + public int provide() { + return 3; + } + } + + public static class Number4Provider implements NumberProvider { + + public int provide() { + return 4; + } + } +}