-
Notifications
You must be signed in to change notification settings - Fork 60
[그리디] 하수한 사다리 미션 제출합니다. #83
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
a8bb2bb
4f5f242
43cdd44
b3f2321
d228d82
2994967
bd78398
d06f91d
784e390
5a0159e
f1244e4
5ef1716
2573545
d3d50f7
82d4c5c
854f13a
9648d42
7650568
bf3978b
a16fcf5
99a4d60
e04642a
1814a51
33d7096
7933714
4f1af17
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,28 @@ | ||
| # java-ladder-func-playground | ||
|
|
||
| 사다리 미션 | ||
|
|
||
| ## 개요 | ||
| 사다리 미션은 간단한 콘솔 애플리케이션으로, 사용자로부터 참여할 사람의 이름과 실행 결과, 최대 사다리 높이를 입력 받습니다.\ | ||
| 그리고 생성된 사다리에 대해 사용자는 참가자의 이름을 입력하여 해당 참가자에 대한 실행 결과를 확인할 수 있고, `all`을 입력하여 전체 결과를 확인할 수 있습니다. | ||
|
|
||
| ## 프로젝트 구조 | ||
| ``` | ||
| controller/ | ||
| - LadderController.java : 전반적인 게임 Flow(입력, 실행, 출력)를 담당하는 클래스 | ||
| model/ | ||
| - ladder/ | ||
| - Connection.java : 사다리 가로 줄의 지점 간 연결을 정의하는 클래스 | ||
| - Ladder.java : 전체 사다리를 정의하는 클래스 | ||
| - LadderFactory.java : 사다리 생성을 담당하는 클래스 | ||
| - Line.java : 사다리의 가로 줄을 정의하는 클래스 | ||
| - Game.java : 게임 실행을 담당하는 클래스 | ||
| - GameConfiguration.java : 게임 설정을 정의하는 클래스 | ||
| - GameConfigurationBuilder.java : 게임 설정 Builder 클래스 | ||
| - GameResult.java : 게임 결과를 정의하는 클래스 | ||
| - Participant.java : 각 참가자를 정의하는 클래스 | ||
| view/ | ||
| - InputView.java : 사용자 입력 처리 기능 수행 | ||
| - OutputView.java : 사용자에게 정보 출력 기능 수행 | ||
| Main.java : Main entrypoint | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package io.suhan.ladder; | ||
|
|
||
| import io.suhan.ladder.controller.LadderController; | ||
|
|
||
| public class Main { | ||
| public static void main(String[] args) { | ||
| LadderController controller = new LadderController(); | ||
| controller.run(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package io.suhan.ladder.controller; | ||
|
|
||
| import io.suhan.ladder.model.Game; | ||
| import io.suhan.ladder.model.GameConfiguration; | ||
| import io.suhan.ladder.model.GameConfigurationBuilder; | ||
| import io.suhan.ladder.model.GameResult; | ||
| import io.suhan.ladder.model.Outcome; | ||
| import io.suhan.ladder.model.Participant; | ||
| import io.suhan.ladder.view.InputView; | ||
| import io.suhan.ladder.view.OutputView; | ||
| import java.util.List; | ||
| import java.util.stream.Stream; | ||
|
|
||
| public class LadderController { | ||
| public void run() { | ||
| try { | ||
| GameConfiguration configuration = readConfiguration(); | ||
| Game game = Game.of(configuration); | ||
|
|
||
| GameResult result = game.execute(); | ||
|
|
||
| OutputView.printLadderResult(game); | ||
|
|
||
| handleOutcomeQuery(result); | ||
| } catch (Exception e) { | ||
| System.out.println(e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| private GameConfiguration readConfiguration() { | ||
| // method chaining으로 구성하려고 하였으나 Input 검증으로 인해 각각 따로 받음 | ||
| GameConfigurationBuilder builder = new GameConfigurationBuilder(); | ||
|
|
||
| List<Participant> participants = InputView.getParticipants().stream().map(Participant::new).toList(); | ||
|
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. 입력받은 String을 List로 변환하는 과정을 두 단계로 나누어 해당 로직을 모두 controller에서 진행하지 않고, 단계 별로 계층을 다르게 한 이유가 궁금합니다! 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. 추가로 participant 입력에 대한 검증 로직이 없어
에 예외를 던지지 않고 다음 단계 입력 로직이 실행되네요! 적절한 위치에 검증 로직을 추가해 위와 같은 경우에도 예외문을 던지면 좋을 것 같은데, 어디에 위치시키면 좋을 것 같나요?
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.
해당 방식의 경우
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.
제가 테스트 해보았을때는 두 케이스(
공백 입력의 경우 #83 (comment) 에서 이어서 다루도록 하겠습니다!! 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.
다시 실행해보니 정상 작동하네요😂 |
||
| builder.participants(participants); | ||
|
|
||
| List<Outcome> outcomes = InputView.getOutcomes().stream().map(Outcome::new).toList(); | ||
| builder.outcomes(outcomes); | ||
|
|
||
| int height = InputView.getLadderHeight(); | ||
| builder.height(height); | ||
|
|
||
| return builder.build(); | ||
| } | ||
|
|
||
| private void handleOutcomeQuery(GameResult result) { | ||
| Stream.generate(InputView::getParticipantForResult) | ||
| .takeWhile((input) -> !input.equals("all")) | ||
| .forEach((input) -> handleSingleQuery(result, input)); | ||
|
|
||
| OutputView.printGameResult(result); | ||
| } | ||
|
|
||
| private void handleSingleQuery(GameResult result, String input) { | ||
| Participant participant = findParticipantByName(result, input); | ||
|
|
||
| if (participant == null) { | ||
| System.out.println("존재하지 않는 참가자입니다."); | ||
| return; | ||
| } | ||
|
|
||
| OutputView.printGameResultOf(participant, result); | ||
| } | ||
|
|
||
| private Participant findParticipantByName(GameResult result, String name) { | ||
| return result.results().keySet().stream() | ||
| .filter((participant -> participant.name().equals(name))) | ||
| .findFirst() | ||
| .orElse(null); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package io.suhan.ladder.model; | ||
|
|
||
| import io.suhan.ladder.model.ladder.Connection; | ||
| import io.suhan.ladder.model.ladder.Ladder; | ||
| import io.suhan.ladder.model.ladder.LadderFactory; | ||
| import io.suhan.ladder.model.ladder.Line; | ||
| import io.suhan.ladder.view.OutputView; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| public class Game { | ||
| private final Ladder ladder; | ||
| private final GameConfiguration configuration; | ||
|
|
||
| private Game(GameConfiguration configuration, Ladder ladder) { | ||
| this.configuration = configuration; | ||
| this.ladder = ladder; | ||
| } | ||
|
|
||
| public static Game of(GameConfiguration configuration) { | ||
| return new Game(configuration, LadderFactory.createLadder(configuration.width(), configuration.height())); | ||
| } | ||
|
|
||
| public static Game of(GameConfiguration configuration, Ladder ladder) { | ||
| return new Game(configuration, ladder); | ||
| } | ||
|
|
||
| public GameResult execute() { | ||
| List<Participant> participants = configuration.participants(); | ||
| List<Outcome> outcomes = configuration.outcomes(); | ||
| Map<Participant, Outcome> result = new LinkedHashMap<>(); | ||
|
|
||
| for (int start = 0; start < configuration.width(); start++) { | ||
| int end = traverse(start); | ||
| Participant participant = participants.get(start); | ||
| Outcome outcome = outcomes.get(end); | ||
|
|
||
| result.put(participant, outcome); | ||
| } | ||
|
|
||
| return new GameResult(result); | ||
| } | ||
|
|
||
| private int traverse(int start) { | ||
| int col = start; | ||
|
|
||
| for (Line line : ladder.lines()) { | ||
| col = findNextColumn(line, col); | ||
| } | ||
|
|
||
| return col; | ||
| } | ||
|
|
||
| private int findNextColumn(Line line, int col) { | ||
| Optional<Connection> connected = line.connections().stream() | ||
| .filter((connection) -> connection.left() == col || connection.right() == col) | ||
| .findFirst(); | ||
|
|
||
| return connected | ||
| .map((connection) -> getConnectedColumn(connection, col)) | ||
| .orElse(col); | ||
| } | ||
|
|
||
| private int getConnectedColumn(Connection connection, int col) { | ||
| if (connection.left() == col) { | ||
| return connection.right(); | ||
| } | ||
|
|
||
| return connection.left(); | ||
| } | ||
|
|
||
| public Ladder getLadder() { | ||
| return ladder; | ||
| } | ||
|
|
||
| public GameConfiguration getConfiguration() { | ||
| return configuration; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package io.suhan.ladder.model; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public record GameConfiguration(List<Participant> participants, List<Outcome> outcomes, int width, int height) { | ||
| @Override | ||
| public List<Participant> participants() { | ||
| return Collections.unmodifiableList(participants); | ||
| } | ||
|
|
||
| @Override | ||
| public List<Outcome> outcomes() { | ||
| return Collections.unmodifiableList(outcomes); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package io.suhan.ladder.model; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class GameConfigurationBuilder { | ||
| private List<Participant> participants; | ||
| private List<Outcome> outcomes; | ||
| private int height; | ||
|
|
||
| public GameConfigurationBuilder participants(List<Participant> participants) { | ||
| if (participants == null) { | ||
| throw new IllegalStateException("참가자 목록이 설정되지 않았습니다."); | ||
| } | ||
|
|
||
| if (participants.isEmpty()) { | ||
| throw new IllegalArgumentException("참가자 목록은 비어 있을 수 없습니다."); | ||
| } | ||
|
|
||
| this.participants = participants; | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| public GameConfigurationBuilder outcomes(List<Outcome> outcomes) { | ||
| if (outcomes == null) { | ||
| throw new IllegalStateException("결과 목록이 설정되지 않았습니다."); | ||
| } | ||
|
|
||
| if (outcomes.size() != participants.size()) { | ||
| throw new IllegalArgumentException("참가자의 수와 실행 결과의 수는 같아야 합니다."); | ||
| } | ||
|
|
||
| this.outcomes = outcomes; | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| public GameConfigurationBuilder height(int height) { | ||
| if (height <= 0) { | ||
| throw new IllegalArgumentException("높이는 양수여야 합니다."); | ||
| } | ||
|
|
||
| this.height = height; | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| public GameConfiguration build() { | ||
| return new GameConfiguration(participants, outcomes, participants.size(), height); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package io.suhan.ladder.model; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.Map; | ||
|
|
||
| public record GameResult(Map<Participant, Outcome> results) { | ||
| @Override | ||
| public Map<Participant, Outcome> results() { | ||
| return Collections.unmodifiableMap(results); | ||
| } | ||
|
|
||
| public Outcome getOutcome(Participant participant) { | ||
| return results.get(participant); | ||
| } | ||
| } |
|
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. 해당 클래스는 객체로 관리하기에는 이유가 부족해 보입니다.
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. 현재 public GameConfigurationBuilder outcomes(List<String> outcomes) {
if (outcomes == null || outcomes.size() != participants.size()) {
throw new IllegalArgumentException("참가자의 수와 실행 결과의 수는 같아야 합니다.");
}
boolean isBlank = outcomes.stream()
.map(String::trim)
.anyMatch(String::isEmpty);
if (isBlank) {
throw new IllegalArgumentException("결과는 공백일 수 없습니다.");
}
this.outcomes = outcomes;
return this;
}만약 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. 저도 수한님의 의견에 매우 동의합니다!
측면에서 현재처럼 객체를 분리하는 구조를 유지하는 것이 좋을 것 같아요! 좋은 의견 감사합니다👍 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package io.suhan.ladder.model; | ||
|
|
||
| public record Outcome(String value) { | ||
| public Outcome { | ||
| if (value.isBlank()) { | ||
| throw new IllegalArgumentException("결과는 공백일 수 없습니다."); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package io.suhan.ladder.model; | ||
|
|
||
| public record Participant(String name) { | ||
| public static final int PARTICIPANT_NAME_MAX_LENGTH = 5; | ||
|
|
||
| public Participant { | ||
| if (name.length() > PARTICIPANT_NAME_MAX_LENGTH) { | ||
| throw new IllegalArgumentException("참가자의 이름은 최대 " + PARTICIPANT_NAME_MAX_LENGTH + "자만 가능합니다."); | ||
| } | ||
|
|
||
| if (name.isBlank()) { | ||
| throw new IllegalArgumentException("참가자의 이름은 공백일 수 없습니다."); | ||
| } | ||
|
Comment on lines
+7
to
+13
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. 수한님께서 어떤 검증 로직은 도메인 값 자체의 유효성 검증은 도메인 내부에서 이루어졌다고 한다면(Participant나 Outcome)
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. 구현 당시에는 객체 자체에 대한 검증(e.g., 하지만 말씀하신대로 따라서 해당 부분에 대해서 생각을 해보았는데, 만약 제 구현 의도대로라면
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package io.suhan.ladder.model.ladder; | ||
|
|
||
| public record Connection(int left, int right) { } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package io.suhan.ladder.model.ladder; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public record Ladder(List<Line> lines) { | ||
| @Override | ||
| public List<Line> lines() { | ||
| return Collections.unmodifiableList(lines); | ||
| } | ||
| } |
|
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. 메서드 호출 체인이 다소 깊은 것 같은데
위 요구사항 때문인가요? 저도 비슷한 로직 (createLine와 createConnections)은 합치려고 고민해보았지만 depth를 1까지만 허용한다는 조건때문에 쉽지 않은 것 같긴하네요🤔
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. 네 맞습니다! 사실 두 로직이 원래 하나의 메소드로 구성되어 있었는데, 해당 조건을 만족하기 위해서 지금과 같은 형태로 변경하게 되었습니다. 안그래도 로직을 나누면서 각 메소드 자체의 길이는 줄었지만, 메소드 내 호출되는 다른 메소드를 따라가면서 이해해야 하는 과정이 추가되었다는 점에서 좀 아쉽다는 생각이 들긴 했습니다 ㅠ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package io.suhan.ladder.model.ladder; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Random; | ||
|
|
||
| public class LadderFactory { | ||
| private static final Random DEFAULT_RANDOM = new Random(); | ||
|
|
||
| public static Ladder createLadder(int width, int height) { | ||
| return createLadder(width, height, DEFAULT_RANDOM); | ||
| } | ||
|
|
||
| public static Ladder createLadder(int width, int height, Random random) { | ||
| List<Line> lines = createLines(width, height, random); | ||
|
|
||
| return new Ladder(lines); | ||
| } | ||
|
|
||
| private static List<Line> createLines(int width, int height, Random random) { | ||
| List<Line> lines = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < height; i++) { | ||
| lines.add(createLine(width, random)); | ||
| } | ||
|
|
||
| return lines; | ||
| } | ||
|
|
||
| private static Line createLine(int width, Random random) { | ||
| List<Connection> connections = createConnections(width, random); | ||
|
|
||
| return new Line(connections); | ||
| } | ||
|
|
||
| private static List<Connection> createConnections(int width, Random random) { | ||
| List<Connection> connections = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < width - 1; i++) { | ||
| if (!shouldConnect(random)) continue; | ||
|
|
||
| connections.add(new Connection(i, i + 1)); | ||
| i += 1; // skip the right next line | ||
| } | ||
|
|
||
| return connections; | ||
| } | ||
|
|
||
| private static boolean shouldConnect(Random random) { | ||
| return random.nextBoolean(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package io.suhan.ladder.model.ladder; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public record Line(List<Connection> connections) { | ||
| @Override | ||
| public List<Connection> connections() { | ||
| return Collections.unmodifiableList(connections); | ||
| } | ||
| } |

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.
개별 값(
participants,outcomes,width,height)을 바로 넘기지 않고,GameConfiguration객체를 통해 입력값을 한 번에 전달하도록 설계하신 이유가 궁금합니다!Uh oh!
There was an error while loading. Please reload this page.
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.
제가 생각해보았을때
GameConfiguration객체를 도입한 이유는 다음과 같습니다:입력 검증 로직과 게임 로직 분리
현재
GameConfiguration객체의 경우GameConfigurationBuilder를 통해 생성되도록 의도하였는데, 검증 로직의 경우 빌더가 담당함으로써 컨트롤러에서는 게임 실행 자체만을 담당할 수 있도록 구성하였습니다.입력 값에 대한 순차적 검증
사실 빌더 패턴을 도입하게된 이유라고 볼 수도 있을 것 같은데, 기존
validate()함수와 같은 방식으로 사용하게 되면, 아래와 같이 검증이 필요한 값들에 대하여 대체로 한 곳에서 검증을 수행합니다.해당 방식의 경우 검증할 값이 많아질 수록 가독성이 떨어진다고 판단했고, 값이 많아짐에 따라 메소드 별로 분리하는 방법도 생각해볼 수 있으나 이 방식 또한 결국 각각의 검증 메소드를 호출해야 한다는 부분이 효율적이지 못하다고 판단했습니다.
따라서 빌더를 통해 값 할당과 동시에 검증 로직을 수행하는 방식이 위 두가지 단점을 해결할 수 있다고 판단하여 빌더 패턴 방식을 도입해보았습니다.
Game생성의 단순화만약 인자 수가 많아진다면 인자의 순서가 헷갈릴 수도 있고, 새로운 필드가 추가된다면 이에 따라 계속 수정해야 한다는 것이 번거롭다고 판단했습니다. 따라서 객체 하나만을 넘겨주는 방식을 통해 이러한 문제를 해결할 수 있었습니다.
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.
로직 생성의 이유를 명확히 고민하고 코드로 잘 구현하신 점이 정말 좋네용👍
2번에 관해서는
라는 의견에 동의합니다! builder로 검증 로직을 분리하여 현재는 도메인이 더욱 비즈니스 로직에 집중할 수 있도록 설계된 것 같아요.
다만 매우매우 개인적으로는 도메인이 유효성까지 스스로 보장하는 것도 DDD 관점에서 괜찮은 접근이라고 생각합니다! 그래도 지금은 설계 의도도 명확하고, 충분히 적절해 보여요👍