diff --git a/README.md b/README.md index 491aece1..d7175610 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ -# java-racingcar-precourse \ No newline at end of file +# java-racingcar-precourse + +헥사고날 아키텍처 연습\ +일반적인 RESTful api 서버가 M, C를 담당하고 브라우저가 V를 담당하니 비슷하게 구성\ +Proxy를 통해 스프링의 @RequestBody처럼 컨트롤러에 들어온 데이터를 알맞게 변환하면 좋겠지만 V에서 바로 변환 + +1. domain entity 작성 +2. port, dto 작성 +3. service 작성 +4. adaptor 작성 +5. view 작성 +6. config 작성 +7. application 작성 \ No newline at end of file diff --git a/src/main/java/katecam/Application.java b/src/main/java/katecam/Application.java new file mode 100644 index 00000000..b53d7a33 --- /dev/null +++ b/src/main/java/katecam/Application.java @@ -0,0 +1,26 @@ +package katecam; + +import katecam.racingcar.adapter.in.RacingCarController; +import katecam.config.AppConfig; + +public class Application { + private static final RacingCarController racingCarController = AppConfig.getRacingCarController(); + private static final RacingCarView racingCarView = new RacingCarView(); + + public static void main(String[] args) { + boolean valid = false; + while (!valid) + try{ + racingCarController.initGame(racingCarView.getCarNames(), racingCarView.getNumberToAttempt()); + racingCarView.display("\n실행 결과"); + valid = true; + }catch (IllegalArgumentException e){ + racingCarView.display(e.getMessage()); + } + do{ + racingCarController.playTurn(); + racingCarView.display(racingCarController.getTurnResult()); + }while(!racingCarController.isEnded()); + racingCarView.display(racingCarController.getTotalResult()); + } +} diff --git a/src/main/java/katecam/RacingCarView.java b/src/main/java/katecam/RacingCarView.java new file mode 100644 index 00000000..87e6ea2e --- /dev/null +++ b/src/main/java/katecam/RacingCarView.java @@ -0,0 +1,37 @@ +package katecam; + +import java.util.Arrays; +import java.util.InputMismatchException; +import java.util.List; +import java.util.Scanner; + +public class RacingCarView { + private final Scanner scanner; + public RacingCarView() { + this.scanner = new Scanner(System.in); + } + + public List getCarNames(){ + display("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + return Arrays.stream(scanner.nextLine().split(",")) + .map(String::trim) + .toList(); + } + + public int getNumberToAttempt(){ + display("시도할 회수는 몇회인가요?"); + int numberToAttempt; + try{ + numberToAttempt = scanner.nextInt(); + }catch (InputMismatchException e){ + throw new IllegalArgumentException("숫자를 입력하세요"); + }finally { + scanner.nextLine(); + } + return numberToAttempt; + } + + public void display(String string){ + System.out.println(string); + } +} diff --git a/src/main/java/katecam/config/AppConfig.java b/src/main/java/katecam/config/AppConfig.java new file mode 100644 index 00000000..b1025e4b --- /dev/null +++ b/src/main/java/katecam/config/AppConfig.java @@ -0,0 +1,39 @@ +package katecam.config; + +import katecam.racingcar.adapter.in.RacingCarController; +import katecam.racingcar.adapter.out.InMemoryGameAdaptor; +import katecam.racingcar.application.port.in.command.GameInitializationUseCase; +import katecam.racingcar.application.port.in.command.GameTurnPlayUseCase; +import katecam.racingcar.application.port.in.query.GameStatusQuery; +import katecam.racingcar.application.port.out.GameRecordPort; +import katecam.racingcar.application.port.out.GameLoadPort; +import katecam.racingcar.application.service.GameInitializationService; +import katecam.racingcar.application.service.GameTurnPlayService; +import katecam.racingcar.application.service.GameStatusQueryService; + +public class AppConfig { + private static GameLoadPort gameLoadPort; + private static GameRecordPort gameRecordPort; + private static GameInitializationUseCase gameInitializationUseCase; + private static GameTurnPlayUseCase gameTurnPlayUseCase; + private static GameStatusQuery gameStatusQuery; + private static RacingCarController racingCarController; + + static { + InMemoryGameAdaptor inMemoryGameAdaptor = new InMemoryGameAdaptor(); + gameRecordPort = inMemoryGameAdaptor; + gameLoadPort = inMemoryGameAdaptor; + gameInitializationUseCase = new GameInitializationService(gameRecordPort); + gameTurnPlayUseCase = new GameTurnPlayService(gameLoadPort); + gameStatusQuery = new GameStatusQueryService(gameLoadPort); + racingCarController = new RacingCarController( + gameInitializationUseCase, + gameTurnPlayUseCase, + gameStatusQuery + ); + } + + public static RacingCarController getRacingCarController() { + return racingCarController; + } +} \ No newline at end of file diff --git a/src/main/java/katecam/racingcar/adapter/in/RacingCarController.java b/src/main/java/katecam/racingcar/adapter/in/RacingCarController.java new file mode 100644 index 00000000..6aab182a --- /dev/null +++ b/src/main/java/katecam/racingcar/adapter/in/RacingCarController.java @@ -0,0 +1,36 @@ +package katecam.racingcar.adapter.in; + +import java.util.List; +import katecam.racingcar.application.dto.command.GameInitializationReq; +import katecam.racingcar.application.port.in.command.GameInitializationUseCase; +import katecam.racingcar.application.port.in.command.GameTurnPlayUseCase; +import katecam.racingcar.application.port.in.query.GameStatusQuery; + +public class RacingCarController { + private final GameInitializationUseCase gameInitializationUseCase; + private final GameTurnPlayUseCase gameTurnPlayUseCase; + private final GameStatusQuery gameStatusQuery; + + public RacingCarController(GameInitializationUseCase gameInitializationUseCase, + GameTurnPlayUseCase gameTurnPlayUseCase, GameStatusQuery gameStatusQuery) { + this.gameInitializationUseCase = gameInitializationUseCase; + this.gameTurnPlayUseCase = gameTurnPlayUseCase; + this.gameStatusQuery = gameStatusQuery; + } + + public void initGame(List carNames, int numberToAttempt){ + gameInitializationUseCase.initialize(new GameInitializationReq(carNames, numberToAttempt)); + } + public void playTurn(){ + gameTurnPlayUseCase.playTurn(); + } + public boolean isEnded(){ + return gameStatusQuery.isEnded(); + } + public String getTurnResult(){ + return gameStatusQuery.getTurnResult().toString(); + } + public String getTotalResult(){ + return gameStatusQuery.getTotalResult().toString(); + } +} \ No newline at end of file diff --git a/src/main/java/katecam/racingcar/adapter/out/InMemoryGameAdaptor.java b/src/main/java/katecam/racingcar/adapter/out/InMemoryGameAdaptor.java new file mode 100644 index 00000000..3c6b5f38 --- /dev/null +++ b/src/main/java/katecam/racingcar/adapter/out/InMemoryGameAdaptor.java @@ -0,0 +1,27 @@ +package katecam.racingcar.adapter.out; + +import katecam.racingcar.application.port.out.GameRecordPort; +import katecam.racingcar.application.port.out.GameLoadPort; +import katecam.racingcar.domain.Game; + +public class InMemoryGameAdaptor implements GameRecordPort, GameLoadPort { + private Game game; + @Override + public void save(Game game) { + if (this.game != null) + throw new IllegalStateException("저장된 게임 있음"); + this.game = game; + } + + @Override + public Game getOrThrow() { + if (this.game == null) + throw new IllegalStateException("저장된 게임 없음"); + return this.game; + } + + @Override + public void delete(){ + this.game = null; + } +} diff --git a/src/main/java/katecam/racingcar/application/dto/command/GameInitializationReq.java b/src/main/java/katecam/racingcar/application/dto/command/GameInitializationReq.java new file mode 100644 index 00000000..800a4a27 --- /dev/null +++ b/src/main/java/katecam/racingcar/application/dto/command/GameInitializationReq.java @@ -0,0 +1,22 @@ +package katecam.racingcar.application.dto.command; + +import java.util.List; + +public record GameInitializationReq( + List carNames, + int numberToAttempt +) { + + public GameInitializationReq{ + if (carNames == null || carNames.isEmpty()) + throw new IllegalArgumentException("리스트 비었음"); + + carNames.forEach(name-> { + if (name == null || name.length() < 5) + throw new IllegalArgumentException("5자리 이상 입력"); + }); + + if (numberToAttempt <= 0) + throw new IllegalArgumentException("올바른 시도 횟수 입력"); + } +} diff --git a/src/main/java/katecam/racingcar/application/dto/query/CarPositionRes.java b/src/main/java/katecam/racingcar/application/dto/query/CarPositionRes.java new file mode 100644 index 00000000..e0dd4259 --- /dev/null +++ b/src/main/java/katecam/racingcar/application/dto/query/CarPositionRes.java @@ -0,0 +1,5 @@ +package katecam.racingcar.application.dto.query; + +public record CarPositionRes(String carName, int position) { + +} diff --git a/src/main/java/katecam/racingcar/application/dto/query/GameTotalResultRes.java b/src/main/java/katecam/racingcar/application/dto/query/GameTotalResultRes.java new file mode 100644 index 00000000..6596daeb --- /dev/null +++ b/src/main/java/katecam/racingcar/application/dto/query/GameTotalResultRes.java @@ -0,0 +1,11 @@ +package katecam.racingcar.application.dto.query; + +import java.util.List; + +public record GameTotalResultRes(List winners) { + + @Override + public String toString() { + return "최종 우승자 : " + String.join(", ", winners); + } +} diff --git a/src/main/java/katecam/racingcar/application/dto/query/GameTurnResultRes.java b/src/main/java/katecam/racingcar/application/dto/query/GameTurnResultRes.java new file mode 100644 index 00000000..609f48dd --- /dev/null +++ b/src/main/java/katecam/racingcar/application/dto/query/GameTurnResultRes.java @@ -0,0 +1,17 @@ +package katecam.racingcar.application.dto.query; + +import java.util.List; + +public record GameTurnResultRes(List carPositions) { + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + this.carPositions.forEach(carPosition -> + sb.append(carPosition.carName()) + .append(" : ") + .append("-".repeat(carPosition.position())) + .append("\n") + ); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/katecam/racingcar/application/port/in/command/GameInitializationUseCase.java b/src/main/java/katecam/racingcar/application/port/in/command/GameInitializationUseCase.java new file mode 100644 index 00000000..14da3b9d --- /dev/null +++ b/src/main/java/katecam/racingcar/application/port/in/command/GameInitializationUseCase.java @@ -0,0 +1,7 @@ +package katecam.racingcar.application.port.in.command; + +import katecam.racingcar.application.dto.command.GameInitializationReq; + +public interface GameInitializationUseCase { + void initialize(GameInitializationReq req); +} diff --git a/src/main/java/katecam/racingcar/application/port/in/command/GameTurnPlayUseCase.java b/src/main/java/katecam/racingcar/application/port/in/command/GameTurnPlayUseCase.java new file mode 100644 index 00000000..1b90ea8f --- /dev/null +++ b/src/main/java/katecam/racingcar/application/port/in/command/GameTurnPlayUseCase.java @@ -0,0 +1,5 @@ +package katecam.racingcar.application.port.in.command; + +public interface GameTurnPlayUseCase { + void playTurn(); +} diff --git a/src/main/java/katecam/racingcar/application/port/in/query/GameStatusQuery.java b/src/main/java/katecam/racingcar/application/port/in/query/GameStatusQuery.java new file mode 100644 index 00000000..d6938719 --- /dev/null +++ b/src/main/java/katecam/racingcar/application/port/in/query/GameStatusQuery.java @@ -0,0 +1,10 @@ +package katecam.racingcar.application.port.in.query; + +import katecam.racingcar.application.dto.query.GameTotalResultRes; +import katecam.racingcar.application.dto.query.GameTurnResultRes; + +public interface GameStatusQuery { + boolean isEnded(); + GameTurnResultRes getTurnResult(); + GameTotalResultRes getTotalResult(); +} diff --git a/src/main/java/katecam/racingcar/application/port/out/GameLoadPort.java b/src/main/java/katecam/racingcar/application/port/out/GameLoadPort.java new file mode 100644 index 00000000..e88d82eb --- /dev/null +++ b/src/main/java/katecam/racingcar/application/port/out/GameLoadPort.java @@ -0,0 +1,7 @@ +package katecam.racingcar.application.port.out; + +import katecam.racingcar.domain.Game; + +public interface GameLoadPort { + Game getOrThrow(); +} diff --git a/src/main/java/katecam/racingcar/application/port/out/GameRecordPort.java b/src/main/java/katecam/racingcar/application/port/out/GameRecordPort.java new file mode 100644 index 00000000..6bd4fc31 --- /dev/null +++ b/src/main/java/katecam/racingcar/application/port/out/GameRecordPort.java @@ -0,0 +1,8 @@ +package katecam.racingcar.application.port.out; + +import katecam.racingcar.domain.Game; + +public interface GameRecordPort { + void save(Game game); + void delete(); +} diff --git a/src/main/java/katecam/racingcar/application/service/GameInitializationService.java b/src/main/java/katecam/racingcar/application/service/GameInitializationService.java new file mode 100644 index 00000000..ad09193e --- /dev/null +++ b/src/main/java/katecam/racingcar/application/service/GameInitializationService.java @@ -0,0 +1,20 @@ +package katecam.racingcar.application.service; + +import katecam.racingcar.application.dto.command.GameInitializationReq; +import katecam.racingcar.application.port.in.command.GameInitializationUseCase; +import katecam.racingcar.application.port.out.GameRecordPort; +import katecam.racingcar.domain.Game; + +public class GameInitializationService implements GameInitializationUseCase { + private final GameRecordPort gameRecordPort; + + public GameInitializationService(GameRecordPort gameRecordPort) { + this.gameRecordPort = gameRecordPort; + } + + @Override + public void initialize(GameInitializationReq req) { + Game game = new Game(req.carNames(), req.numberToAttempt()); + gameRecordPort.save(game); + } +} diff --git a/src/main/java/katecam/racingcar/application/service/GameStatusQueryService.java b/src/main/java/katecam/racingcar/application/service/GameStatusQueryService.java new file mode 100644 index 00000000..7094aea8 --- /dev/null +++ b/src/main/java/katecam/racingcar/application/service/GameStatusQueryService.java @@ -0,0 +1,43 @@ +package katecam.racingcar.application.service; + + +import java.util.List; +import katecam.racingcar.application.dto.query.CarPositionRes; +import katecam.racingcar.application.dto.query.GameTotalResultRes; +import katecam.racingcar.application.dto.query.GameTurnResultRes; +import katecam.racingcar.application.port.in.query.GameStatusQuery; +import katecam.racingcar.application.port.out.GameLoadPort; +import katecam.racingcar.domain.Car; +import katecam.racingcar.domain.Game; + +public class GameStatusQueryService implements GameStatusQuery { + private final GameLoadPort gameLoadPort; + + public GameStatusQueryService(GameLoadPort gameLoadPort) { + this.gameLoadPort = gameLoadPort; + } + + @Override + public boolean isEnded() { + Game game = gameLoadPort.getOrThrow(); + return game.isEnded(); + } + + @Override + public GameTurnResultRes getTurnResult() { + Game game = gameLoadPort.getOrThrow(); + List carPositions= game.getCars().stream() + .map(car->new CarPositionRes(car.getName(), car.getPosition())) + .toList(); + return new GameTurnResultRes(carPositions); + } + + @Override + public GameTotalResultRes getTotalResult() { + Game game = gameLoadPort.getOrThrow(); + List winnerNames = game.getWinners().stream() + .map(Car::getName) + .toList(); + return new GameTotalResultRes(winnerNames); + } +} diff --git a/src/main/java/katecam/racingcar/application/service/GameTurnPlayService.java b/src/main/java/katecam/racingcar/application/service/GameTurnPlayService.java new file mode 100644 index 00000000..0d25df58 --- /dev/null +++ b/src/main/java/katecam/racingcar/application/service/GameTurnPlayService.java @@ -0,0 +1,19 @@ +package katecam.racingcar.application.service; + +import katecam.racingcar.application.port.in.command.GameTurnPlayUseCase; +import katecam.racingcar.application.port.out.GameLoadPort; +import katecam.racingcar.domain.Game; + +public class GameTurnPlayService implements GameTurnPlayUseCase { + private final GameLoadPort gameLoadPort; + + public GameTurnPlayService(GameLoadPort gameLoadPort) { + this.gameLoadPort = gameLoadPort; + } + + @Override + public void playTurn() { + Game game = gameLoadPort.getOrThrow(); + game.playTurn(); + } +} diff --git a/src/main/java/katecam/racingcar/domain/Car.java b/src/main/java/katecam/racingcar/domain/Car.java new file mode 100644 index 00000000..6c591cac --- /dev/null +++ b/src/main/java/katecam/racingcar/domain/Car.java @@ -0,0 +1,34 @@ +package katecam.racingcar.domain; + + +import java.util.Random; + +public final class Car { + private final String name; + private final int position; + private static final Random random; + static { + random = new Random(); + } + + public Car(String name) { + this(name, 0); + } + + public Car(String name, int position) { + this.name = name; + this.position = position; + } + + public String getName() { + return name; + } + + public Car move(){ + return (random.nextInt(10) >= 4) ? new Car(this.name, this.position + 1) : this; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/katecam/racingcar/domain/Cars.java b/src/main/java/katecam/racingcar/domain/Cars.java new file mode 100644 index 00000000..82a4195a --- /dev/null +++ b/src/main/java/katecam/racingcar/domain/Cars.java @@ -0,0 +1,38 @@ +package katecam.racingcar.domain; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public final class Cars { + private final List cars; + + private Cars(List cars) { + this.cars = cars; + } + + public static Cars of(List carNames){ + return new Cars(carNames.stream() + .map(Car::new) + .collect(Collectors.toList())); //Cars를 불변 객체로 만들지 않았으므로 수정될 수 있는 List를 만들어야 함 + } + + public void moveAll(){ + IntStream.range(0, cars.size()) + .forEach(i-> cars.set(i, cars.get(i).move())); + } + public List getWinners(){ + return this.cars.stream() + .collect(Collectors.groupingBy(Car::getPosition)) + .entrySet().stream() + .max(Map.Entry.comparingByKey()) + .map(Entry::getValue) + .orElseGet(List::of); + } + public List getCars(){ + return Collections.unmodifiableList(cars); + } +} diff --git a/src/main/java/katecam/racingcar/domain/Game.java b/src/main/java/katecam/racingcar/domain/Game.java new file mode 100644 index 00000000..fdc94d69 --- /dev/null +++ b/src/main/java/katecam/racingcar/domain/Game.java @@ -0,0 +1,41 @@ +package katecam.racingcar.domain; + +import java.util.List; + +public class Game { + private final Cars cars; + private final int numberToAttempt; + private int attemptedNumber; + + public Game(List carNames, int carNumber) { + this.cars = Cars.of(carNames); + this.numberToAttempt = carNumber; + } + + public List getCars(){ + return this.cars.getCars(); + } + + public List getWinners(){ + if (!isEnded()) throw new IllegalStateException("게임 종료 안됨"); + return cars.getWinners(); + } + + public void playTurn() { + if (isEnded()) throw new IllegalStateException("게임이 이미 종료됨"); + cars.moveAll(); + attemptedNumber++; + } + + public boolean isEnded(){ + return this.attemptedNumber == this.numberToAttempt; + } + + public int getNumberToAttempt() { + return numberToAttempt; + } + + public int getAttemptedNumber() { + return attemptedNumber; + } +} diff --git a/src/test/java/katecam/racingcar/application/GameInitializationServiceTest.java b/src/test/java/katecam/racingcar/application/GameInitializationServiceTest.java new file mode 100644 index 00000000..47f1ad2d --- /dev/null +++ b/src/test/java/katecam/racingcar/application/GameInitializationServiceTest.java @@ -0,0 +1,35 @@ +package katecam.racingcar.application; + +import java.util.List; +import katecam.racingcar.adapter.out.InMemoryGameAdaptor; +import katecam.racingcar.application.dto.command.GameInitializationReq; +import katecam.racingcar.application.service.GameInitializationService; +import katecam.racingcar.domain.Car; +import katecam.racingcar.domain.Game; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class GameInitializationServiceTest { + private InMemoryGameAdaptor inMemoryGameAdaptor; + private GameInitializationService service; + + @BeforeEach + void setUp() { + inMemoryGameAdaptor = new InMemoryGameAdaptor(); + service = new GameInitializationService(inMemoryGameAdaptor); + } + + @Test + void 초기화_테스트() { + List carNames = List.of("sdfasdf","asdfasfd","xcvsdqeg"); + GameInitializationReq req = new GameInitializationReq(carNames, 5); + service.initialize(req); + + Game game = inMemoryGameAdaptor.getOrThrow(); + List cars = game.getCars(); + + Assertions.assertEquals(cars.size(), carNames.size()); + } + +} diff --git a/src/test/java/katecam/racingcar/application/GameTurnPlayServiceTest.java b/src/test/java/katecam/racingcar/application/GameTurnPlayServiceTest.java new file mode 100644 index 00000000..f5281b88 --- /dev/null +++ b/src/test/java/katecam/racingcar/application/GameTurnPlayServiceTest.java @@ -0,0 +1,48 @@ +package katecam.racingcar.application; + +import java.util.List; +import katecam.racingcar.adapter.out.InMemoryGameAdaptor; +import katecam.racingcar.application.dto.command.GameInitializationReq; +import katecam.racingcar.application.service.GameInitializationService; +import katecam.racingcar.application.service.GameTurnPlayService; +import katecam.racingcar.domain.Game; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class GameTurnPlayServiceTest { + private final int NUMBER_TO_ATTEMPT = 5; + private GameTurnPlayService service; + private Game game; + + @BeforeEach + void setUp() { + InMemoryGameAdaptor inMemoryGameAdaptor = new InMemoryGameAdaptor(); + service = new GameTurnPlayService(inMemoryGameAdaptor); + + List carNames = List.of("sdfasdf","asdfasfd","xcvsdqeg"); + GameInitializationReq req = new GameInitializationReq(carNames, NUMBER_TO_ATTEMPT); + GameInitializationService initializationService = new GameInitializationService( + inMemoryGameAdaptor); + initializationService.initialize(req); + + game = inMemoryGameAdaptor.getOrThrow(); + } + + @Test + void 플레이턴_테스트() { + service.playTurn(); + + Assertions.assertEquals(1, game.getAttemptedNumber()); + } + + @Test + void 게임끝날때까지돌리기(){ + for (int i = 0; i < NUMBER_TO_ATTEMPT; i++) { + service.playTurn(); + } + + Assertions.assertTrue(game.isEnded()); + + } +}