-
Notifications
You must be signed in to change notification settings - Fork 60
[그리디] 김태우 사다리 미션 제출합니다. #84
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
Changes from all commits
ec68a0e
ab34af1
b5d6346
6425261
b3e4558
e0819f4
8421f74
7bd2af2
94e6bde
af22352
ecac217
1514d48
54ddd4d
99aa533
d8ce27f
cbd131e
5d6dfca
fb63a02
a7b8c81
ffe782d
fb624ea
851055c
6a287b1
5d7fbc9
2ff5f21
6fa0b0d
bf10258
5326b93
9317192
339963b
5570d3e
9507b6b
f5e4d9d
370c1c8
b4a8608
ab293fe
fdb8c20
4f5bc72
4db9002
e23bafd
7e4d366
ff4df5e
33e9c62
e023d6d
ff8a8db
bf23911
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 사다리 게임 | ||
|
|
||
| ## 💡 기능 요구사항 | ||
|
|
||
| 1. 사다리 게임에 참여하는 사람에 이름을 최대 5글자까지 부여할 수 있다. | ||
| 2. 사다리를 출력할 때 사람 이름도 같이 출력한다. | ||
| 3. 사람 이름은 쉼표(,)를 기준으로 구분한다. | ||
| 4. 개인별 이름을 입력하면 개인별 결과를 출력한다. | ||
| 5. `"all"`을 입력하면 전체 참여자의 실행 결과를 출력한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 클래스 소개 | ||
|
|
||
| | 클래스 | 역할| | ||
| |----------------------------|---| | ||
| | `Name` | 이름 단위를 표현 | | ||
| | `Names` | 이름들의 일급 컬렉션 | | ||
| | `Players` | 참가자 이름 목록 관리 및 중복 제거 | | ||
| | `Results` | 실행 결과 목록 관리 | | ||
| | `Height` | 사다리 높이 검증 및 저장 | | ||
| | `Connect` | `CONNECTED` / `DISCONNECTED` 상태 Enum | | ||
| | `Point` | 연결 여부(`Connect`) 표현 | | ||
| | `Line` | 한 줄의 연결 상태(`Point`) 관리 | | ||
| | `Ladder` | 사다리 전체를 구성 (여러 Line의 집합) | | ||
| | `LadderGame` | 이동 로직 및 결과 매핑 수행 | | ||
| | `LadderController` | 전체 게임 실행 흐름 제어 | | ||
| | `InputView` / `OutputView` | 입출력 담당 | | ||
| | `Main` | 프로그램 실행의 진입점 담당 | | ||
|
|
||
| --- | ||
|
|
||
| ## 프로그램 실행 흐름 | ||
|
|
||
| 1. **입력 단계** | ||
| - 참여자 이름, 실행 결과, 사다리 높이를 순서대로 입력받는다. | ||
| - 이름과 결과의 개수가 다르면 예외를 발생시킨다. | ||
| - 이름이 5글자를 초과하면 예외를 발생한다. | ||
| - 사다리 높이가 양수가 아니거나 숫자가 아닌 입력이 들어오면 예외를 발생시킨다. | ||
|
|
||
| --- | ||
|
|
||
| 2. **사다리 생성 단계** | ||
| - 입력받은 높이와 인원 수를 기반으로 `Ladder` 객체를 생성한다. | ||
| - 내부적으로 `Line.create()`를 호출하여 각 Line의 연결 상태를 무작위로 생성한다. | ||
| - 높이가 0인 경우에도 1줄의 사다리가 생성된다. | ||
|
|
||
| --- | ||
|
|
||
| 3. **결과 조회 단계** | ||
| - 사용자가 이름을 입력하면 해당 참가자의 결과를 출력한다. | ||
| - `"all"`을 입력하면 전체 참가자의 결과를 한 번에 출력한다. | ||
|
|
||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import controller.LadderController; | ||
| import domain.LadderGame; | ||
| import domain.Players; | ||
| import domain.Results; | ||
| import view.InputView; | ||
| import view.OutputView; | ||
|
|
||
| public class Main { | ||
| public static void main(String[] args) { | ||
| InputView inputView = new InputView(); | ||
| OutputView outputView = new OutputView(); | ||
| LadderController controller = new LadderController(inputView, outputView); | ||
|
|
||
| Players players = controller.inputPlayers(); | ||
| Results results = controller.inputResults(players); | ||
| int height = controller.inputHeight(); | ||
|
|
||
| LadderGame game = controller.startLadderGame(height, players, results); | ||
| controller.showResult(game, players); | ||
|
|
||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package controller; | ||
|
|
||
| import domain.Ladder; | ||
| import domain.LadderGame; | ||
| import domain.PlayerName; | ||
| import domain.Players; | ||
| import domain.ResultName; | ||
| import domain.Results; | ||
| import view.InputView; | ||
| import view.OutputView; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.InputMismatchException; | ||
| import java.util.List; | ||
| import java.util.Random; | ||
|
|
||
| public class LadderController { | ||
| private final InputView inputView; | ||
| private final OutputView outputView; | ||
|
|
||
| public LadderController(InputView inputView, OutputView outputView) { | ||
| this.inputView = inputView; | ||
| this.outputView = outputView; | ||
| } | ||
|
|
||
| public Players inputPlayers() { | ||
| outputView.printAskPlayers(); | ||
| while (true) { | ||
| try { | ||
| String input = inputView.readString(); | ||
| List<PlayerName> players = Arrays.stream(input.split(",")) | ||
| .map(String::trim) | ||
| .map(PlayerName::new) | ||
| .toList(); | ||
| return new Players(players); | ||
| } catch (IllegalArgumentException e) { | ||
| outputView.printException(e); | ||
| outputView.printRetryInputMessage(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public Results inputResults(Players players) { | ||
| outputView.printAskResults(); | ||
| while (true) { | ||
| try { | ||
| String input = inputView.readString(); | ||
| List<ResultName> result = Arrays.stream(input.split(",")) | ||
| .map(String::trim) | ||
| .map(ResultName::new) | ||
| .toList(); | ||
| Results results = new Results(result); | ||
| LadderGame.validatePlayerAndResultCount(players, results); | ||
| return results; | ||
| } catch (IllegalArgumentException e) { | ||
| outputView.printException(e); | ||
| outputView.printRetryInputMessage(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public int inputHeight() { | ||
| outputView.printAskHeight(); | ||
| while (true) { | ||
| try { | ||
| int height = inputView.readInt(); | ||
| if (height < 0) { | ||
| throw new IllegalArgumentException("높이는 양수여야 합니다."); | ||
| } | ||
| return height; | ||
| } catch (InputMismatchException | IllegalArgumentException e) { | ||
| outputView.printException(e); | ||
| outputView.printRetryInputMessage(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public LadderGame startLadderGame(int height, Players players, Results results) { | ||
| Ladder ladder = new Ladder(height, players.size(), new Random()); | ||
| LadderGame game = new LadderGame(ladder, players, results); | ||
| outputView.printLadderResultTitle(); | ||
| outputView.printLadder(ladder, players.getPlayers(), results.getResults()); | ||
| return game; | ||
| } | ||
|
|
||
| public void showResult(LadderGame game, Players players) { | ||
| boolean run = true; | ||
| while (run) { | ||
| outputView.printAskResultByPlayer(); | ||
| String name = inputView.readString(); | ||
| run = validateRun(game, players, name); | ||
| } | ||
| } | ||
|
|
||
| private boolean validateRun(LadderGame game, Players players, String name) { | ||
| if (name.equals("all")) { | ||
| outputView.printAllResults(game.findAll(), players); | ||
| return false; | ||
| } | ||
| String result = game.findResultByPlayer(name); | ||
| outputView.printSingleResult(result); | ||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package domain; | ||
|
|
||
| public enum Connect { | ||
| CONNECTED(true) { | ||
| @Override | ||
| public int moveRight(int index) { | ||
| return index + 1; | ||
| } | ||
|
|
||
| @Override | ||
| public int moveLeft(int index) { | ||
| return index - 1; | ||
| } | ||
| }, | ||
| DISCONNECTED(false) { | ||
| @Override | ||
| public int moveRight(int index) { | ||
| return index; | ||
| } | ||
|
|
||
| @Override | ||
| public int moveLeft(int index) { | ||
| return index; | ||
| } | ||
| }; | ||
|
|
||
| private final boolean value; | ||
|
|
||
| Connect(boolean value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public abstract int moveRight(int index); | ||
|
|
||
| public abstract int moveLeft(int index); | ||
|
|
||
| public boolean isConnected() { | ||
| return value; | ||
| } | ||
|
|
||
| public static Connect from(boolean value) { | ||
| return value ? CONNECTED : DISCONNECTED; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package domain; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Random; | ||
|
|
||
| public class Ladder { | ||
| private final List<Line> lines; | ||
|
|
||
| public Ladder(int height, int playerCount, Random random) { | ||
| List<Line> temp = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < height; i++) { | ||
| temp.add(Line.create(playerCount, random)); | ||
| } | ||
| if (height == 0) { | ||
| temp.add(Line.create(playerCount, random)); | ||
| } | ||
| this.lines = List.copyOf(temp); | ||
| } | ||
|
|
||
| public List<Line> getLines() { | ||
| return lines; | ||
| } | ||
| } | ||
|
Comment on lines
+7
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 Request Change생성자에서 List 필드를 주입할 때, copyOf를 사용하는 것에 대한 리뷰에 다음과 같이 답해주셨습니다. [질문]태우가 방어적 복사를 사용하는 이유는 무엇인가요? [추가 설명]
private final List<Line> lines;이 필드가 실제로 불변일까요? 저는 lines가 현재 실제로 불변하지도 않고, 불변해야할 필요가 있는지에 대한 확신도 없습니다. Ladder ladder = new Ladder(height, players.size(), new Random(1));
ladder.getLines().add(Line.create(players.size(), new Random(1)));위와 같은 방식으로 ladder가 생성된 이후에 lines를 불러, 이를 만약 막고 싶다면, 제가 참고 자료로 남겼던 블로그에도 소개되는
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. copyOf를 사용한 이유는 해당 도메인(Players / Results / Ladder)이 사다리 게임에서 핵심 역할을 가지고 있고, 그리고 저는 이 컬렉션을 생성 이후 외부에서 add/remove 등으로 조작하는 형태로 사용할 계획이 없었기 때문에, 다만 copyOf는 얕은 복사이기 때문에 저와 같이 “외부에서 컬렉션 조작을 전제로 하지 않은 경우”에도 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package domain; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public class LadderGame { | ||
| private final Ladder ladder; | ||
| private final Players players; | ||
| private final Results results; | ||
|
|
||
| public LadderGame(Ladder ladder, Players players, Results results) { | ||
| this.ladder = ladder; | ||
| this.players = players; | ||
| this.results = results; | ||
| } | ||
|
|
||
| public static void validatePlayerAndResultCount(Players players, Results results) { | ||
| if (players.size() != results.size()) { | ||
| throw new IllegalArgumentException("참가자와 결과 수는 같아야 합니다."); | ||
| } | ||
| } | ||
|
|
||
| private int move(int position) { | ||
| for (Line line : ladder.getLines()) { | ||
| position = line.moveOf(position); | ||
| } | ||
| return position; | ||
| } | ||
|
|
||
|
|
||
| public String findResultByPlayer(String name) { | ||
| List<PlayerName> player = players.getPlayers(); | ||
| List<ResultName> result = results.getResults(); | ||
|
|
||
| int index = player.indexOf(new PlayerName(name)); | ||
| if (index < 0) { | ||
| throw new IllegalArgumentException("존재하지 않는 플레이어입니다."); | ||
| } | ||
| index = move(index); | ||
| return result.get(index).value(); | ||
| } | ||
|
|
||
| public Map<String, String> findAll() { | ||
| Map<String, String> map = new HashMap<>(); | ||
| List<PlayerName> player = players.getPlayers(); | ||
| for (PlayerName name : player) { | ||
| map.put(name.value(), findResultByPlayer(name.value())); | ||
| } | ||
| return map; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package domain; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Random; | ||
|
|
||
| public class Line { | ||
| private final List<Connect> points; | ||
|
|
||
| private Line(List<Connect> points) { | ||
| this.points = List.copyOf(points); | ||
| } | ||
|
|
||
| static Line of(List<Connect> points) { | ||
| return new Line(points); | ||
| } | ||
|
|
||
| public static Line create(int playerCount, Random random) { | ||
| List<Connect> points = new ArrayList<>(); | ||
| Connect prev = Connect.DISCONNECTED; | ||
|
|
||
| for (int i = 0; i < playerCount - 1; i++) { | ||
| Connect next = Connect.from(random.nextBoolean()); | ||
| next = checkPrev(prev, next); | ||
| points.add(next); | ||
| prev = next; | ||
| } | ||
| return new Line(points); | ||
| } | ||
|
|
||
| private static Connect checkPrev(Connect prev, Connect next) { | ||
| if (prev.isConnected()) { | ||
| return Connect.DISCONNECTED; | ||
| } | ||
| return next; | ||
| } | ||
|
|
||
| private Connect rightOf(int index) { | ||
| if (index >= points.size()) return Connect.DISCONNECTED; | ||
| return points.get(index); | ||
| } | ||
|
|
||
| private Connect leftOf(int index) { | ||
| if (index <= 0) return Connect.DISCONNECTED; | ||
| return points.get(index - 1); | ||
| } | ||
|
|
||
| public int moveOf(int index) { | ||
| Connect right = rightOf(index); | ||
| if (right.isConnected()) return right.moveRight(index); | ||
|
|
||
| Connect left = leftOf(index); | ||
| if (left.isConnected()) return left.moveLeft(index); | ||
|
|
||
| return index; | ||
| } | ||
|
|
||
| public List<Connect> getPoints() { | ||
| return points; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀 Comment
위에서 이런 코멘트를 남겨주셨습니다.
저는 태우가 controller에서 입력값을 검증하고,
재요청을 받는것 같은 처리를 한 것이 적절하다고 판단이 됩니다. 굿👍
"validation을 어디서 할까?" 이건 다들 고민하는 영역인 것 같아요.
각 영역에서 검증은 모두 있을 수 있고 목적이 약간씩은 다른 것 같아요.
이런 console 어플리케이션을 구현하다보면 그 경계가 모호하고 규모가 너무 작아 헷갈릴 수 있습니다.
하지만, 꼭 필요한 고민이라고 생각하고 추후에 다른 환경에서 개발할 때도
이렇게 각 영역에서 검증해야되는 것이 어떤것일까 고민하시면 좋은 설계를 할 수 있으실거에요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validation 책임 분리에 대한 조언 감사합니다!
말씀해주신 것처럼 View(InputView)에서의 즉각적인 입력 포맷 검증, Controller에서의 전달 전 검증, Domain 내부의 비즈니스 룰 검증 각각의 영역을 잘 이해하게 됐습니다.
이 관점을 기반으로 나중에 웹 환경으로 확장할 때 validation 위치를 더 의식적으로 선택해보겠습니다!