Skip to content

[이예진] 사다리 게임 구현 (1-5단계) #66

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

Open
wants to merge 5 commits into
base: yaevrai
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# java-ladder : 사다리 - 함수형 프로그래밍

## Level1. 사다리 출력

### - 요구사항

- [x] 네이버 사다리 게임을 참고하여 도메인을 분석하여 구현한다.
- [x] 사다리는 4x4 크기로 고정되고, 연결 여부는 랜덤으로 결정한다.
- [x] 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다.
- [x] 모든 엔티티를 작게 유지한다.
- [x] 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

## Level2. 사다리 생성

### - 요구사항

- [x] 사다리는 크기를 입력 받아 생성할 수 있다. (넓이 & 높이)

## Level3. 사다리 타기

### - 요구사항

- [x] 사다리의 시작 지점과 도착 지점을 출력한다.

## Level4. 게임 실행

### - 요구사항

- [x] 사다리 게임에 참여하는 사람에 이름을 최대 5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다.
- [x] 사람 이름은 쉼표(,)를 기준으로 구분한다.
- [x] 개인별 이름을 입력하면 개인별 결과를 출력하고, "all"을 입력하면 전체 참여자의 실행 결과를 출력한다.

## Level5. 리팩토링

### - 요구사항

- [x] 학습 테스트를 통해 학습한 내용을 반영한다. 자바에서 제공하는 함수형 문법을 적용해보고, 어떠한 차이가 있는지 경험한다.
13 changes: 13 additions & 0 deletions src/main/java/ladder/controller/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ladder.controller;

import ladder.view.InputView;
import ladder.view.OutputView;

public class Application {
public static void main(String[] args) {
InputView inputView = new InputView();
OutputView outputView = new OutputView();
LadderGame game = new LadderGame(inputView, outputView);
game.play();
}
}
67 changes: 67 additions & 0 deletions src/main/java/ladder/controller/LadderGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ladder.controller;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import ladder.model.GameSetup;
import ladder.model.Ladder;
import ladder.model.Participants;
import ladder.model.ResultType;
import ladder.model.Results;
import ladder.view.InputView;
import ladder.view.OutputView;

public class LadderGame {

private final InputView inputView;
private final OutputView outputView;

public LadderGame(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}

public void play() {
GameSetup setup = createGameSetup();
displayLadder(setup);
processResult(setup);
}

private GameSetup createGameSetup() {
Participants participants = inputView.inputParticipants();
List<String> results = inputView.inputResults();
int height = inputView.inputHeight();
Ladder ladder = Ladder.create(participants.size(), height);
return new GameSetup(participants, results, ladder);
}

private void displayLadder(GameSetup setup) {
outputView.printLadder(setup.getLadder(), setup.getParticipants(), setup.getResults());
}

private void processResult(GameSetup setup) {
String result = inputView.inputGameResult();
Results gameResults = createGameResults(setup);
printGameResults(result, gameResults);
}

private Results createGameResults(GameSetup setup) {
List<Integer> ladderResults = setup.getLadder().result();
return new Results(setup.getParticipants(),
mapResults(setup.getResults(), ladderResults));
}

private void printGameResults(String result, Results gameResults) {
if (ResultType.ALL.equals(ResultType.from(result))) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 enum을 비교할 때, equals 보다 ==을 사용하는 것을 권장해요.

이유는 다음과 같은 코드로 변경하고, 어플리케이션을 실행해 보세요.

ResultType.ALL.equals(TimeUnit.HOURS)

그리고 결과를 확인하신 뒤, 다음과 같이 변경해 보시고 어플리케이션을 실행해 보세요.

ResultType.ALL == TimeUnit.HOURS

outputView.printAllResults(gameResults);
return;
}
outputView.printSingleResult(gameResults.getResult(result));
}

private List<String> mapResults(List<String> inputResults, List<Integer> ladderResults) {
return IntStream.range(0, inputResults.size())
.mapToObj(i -> inputResults.get(ladderResults.get(i)))
.collect(Collectors.toList());
}
}
29 changes: 29 additions & 0 deletions src/main/java/ladder/model/GameSetup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ladder.model;

import java.util.ArrayList;
import java.util.List;

public class GameSetup {

private final Participants participants;
private final List<String> results;
private final Ladder ladder;

public GameSetup(Participants participants, List<String> results, Ladder ladder) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LadderGame 클래스에서 play()에서의 메서드 길이를 줄이기 위해서 구현 후에 게임 관련 정보를 담는 커맨드 객체 GameSetup를 만들어서 개선을 했는데요. 애초에 LadderGame 역할 분배가 더 이루어졌으면 메서드 길이를 줄이기 위한 목적으로 객체를 굳이 만들지 않아도 되었을까? 라는 생각은 들었는데 어떻게 개선해야할지 방도가 잘 생각이 안났습니다🤣 이전 미션에서도 그렇고 LadderGame 같이 시작점이 되는 메서드가 여러 역할을 갖는 것 같은데, 또 한편으론 시작점이기 때문에 여러 진행 포인트가 합쳐저 있는게 당연하지않나? 라는 생각도 들어요. 제 질문이.. 잘 이해되실지 모르겠습니다 ㅜㅜ 글렌이라면 어떻게 하셨을 거 같은지 궁금합니다!!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 해당 객체 때문에 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. 라는 요구사항이 지켜지지 못한 것 같네요.

하지만 이러한 시도를 해주시고, 본인이 시도한 방법에 대해 성찰해 보시는 것은 분명 성장하는데 큰 도움이 될 거에요.

우선 저였다면, 미션의 요구사항과 결과를 보고 사다리 게임을 분석했을 것 같아요.

사다리 게임을 간단하게 분석해 보면, 사다리 게임은 사용자의 입력이 완료되면 결과는 결정적이에요.
(사다리의 생성은 랜덤하게 생성되기에 비결정적이지만, 사다리가 생성된 후에는 결과가 절대로 변하지 않아요)

또한 사다리 게임에서 가장 중요한 것은 결과가 제대로 나오는 것인데, 이를 쉽게 테스트할 수 있도록 구조를 만들 것 같아요.
(위 리뷰에서 남겼듯, 사디리의 출력은 그저 사람이 결과를 보기 쉽게 알 수 있도록 해주는 문자열에 불과해요.)

그렇다면 이를 통해, 다음과 같은 클래스를 구현했을 것 같아요.

public class Ladder {

    private final List<Line> lines = ...

    public Ladder(List<Line> lines) {
        // 유효성 체크
        this.lines = lines;
    }

    public LadderResult calculateResult() {
        // 사다리 게임 로직
    }
}

public class LadderResult {

    private final Map<Participant, Result> participantToResult;

    ...

    public Result getResult(Participant participant) {
        // 유효성 체크
        return participantToResult.get(participant);
    }
}

이 클래스를 기반으로 TDD로 구현하거나, 아니면 그저 구현해 본 뒤, 결과가 제대로 나오는 것을 확인한 뒤, 다른 요구사항을 구현하지 않았을까 싶네요.

this.participants = participants;
this.results = new ArrayList<>(results);
this.ladder = ladder;
}

public Participants getParticipants() {
return participants;
}

public List<String> getResults() {
return new ArrayList<>(results);
}

public Ladder getLadder() {
return ladder;
}
}
66 changes: 66 additions & 0 deletions src/main/java/ladder/model/Ladder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ladder.model;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Ladder {

private final List<Line> lines;

private Ladder(List<Line> lines) {
this.lines = lines;
}

public static Ladder create(int width, int height) {
return new Ladder(
IntStream.range(0, height)
.mapToObj(i -> Line.create(width))
.collect(Collectors.toList())
);
}

public void draw() {
for (Line line : lines) {
line.draw();
}
}

public List<Integer> result() {
int width = lines.get(0).getPoints().size() + 1;
return IntStream.range(0, width)
.map(this::getEndPoint)
.boxed()
.collect(Collectors.toList());
}
Comment on lines +29 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사다리 게임의 결과를 출력하는 메서드군요!

그런데 결과의 List<Integer> 타입은 어떤 것을 의미하나요?

Integer가 어떤 의미를 담고 있는지 알 수 있나요?


private int getEndPoint(int start) {
int position = start;

for (Line line : lines) {
position = move(position, line.getPoints());
}

return position;
}

private int move(int position, List<Boolean> points) {
if (canMoveLeft(position, points)) {
return position - 1;
}

if (canMoveRight(position, points)) {
return position + 1;
}

return position;
}

private boolean canMoveLeft(int position, List<Boolean> points) {
return position > 0 && points.get(position - 1);
}

private boolean canMoveRight(int position, List<Boolean> points) {
return position < points.size() && points.get(position);
}
}
50 changes: 50 additions & 0 deletions src/main/java/ladder/model/Line.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ladder.model;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Line {

private final List<Boolean> points;

private Line(List<Boolean> points) {
this.points = points;
}

public List<Boolean> getPoints() {
return new ArrayList<>(points);
}

public static Line create(int width) {
Random random = new Random();
List<Boolean> points = new ArrayList<>();
return new Line(IntStream.range(0, width - 1)
.mapToObj(i -> shouldConnect(points, i, random))
.collect(Collectors.toList()));
}

private static boolean shouldConnect(List<Boolean> points, int index, Random random) {
boolean connection = (index <= 0 || !points.get(index - 1)) && random.nextBoolean();
points.add(connection);
return connection;
}
Comment on lines +21 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사다리 미션의 요구사항 중 핵심 중 하나인 연결 여부를 랜덤으로 정하고, 라인이 겹치지 않도록 하는 곳을 구현하는 코드로 보이네요!

다만 이 코드를 보면 한 번에 보고 이해하기 어려워 보여요.

  1. shouldConnect 메서드의 파라미터가 너무 많아 보이네요. (Random은 자동차 경주 게임 리뷰에서 남겼듯 다른 구현체를 사용한다면 파라미터로 넘길 필요는 없어 보여요)
  2. create 메서드의 points 변수를 생성자로 넘길 것 같은데, 그저 이전에 연결된 상태를 보기 위한 변수네요. 이 경우 다른 이름을 붙여주는 게 더 좋을 것 같아요.
  3. shouldConnect 메서드의 (index <= 0 || !points.get(index - 1)) && random.nextBoolean() 부분이 특히 이해하기 어렵네요.

또한 5단계 미션의 요구사항 때문에 Stream을 사용하신 것 같아요.

이 경우 Stream을 사용하는 것 보다, for문을 사용하는게 더 가독성이 좋았지 않았을까 싶네요. 😂


public void draw() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line이란 객체에게 사다리 라인을 만드는 책임을 부여했는데요,
라인 생성 == 사다리 출력이 되는 부분이라 이 부분이 view 객체로 가야하나? 라는 생각이 들었습니다.
결과적으로는 Line클래스 내에서 처리하는 게 어떻게 라인을 생성해야하는 지에 대한 비즈니스 로직을 알 수 있고, 좀 더 명확해 보인다고 생각했습니다 😅

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사다리 게임은 사용자의 입력이 완료되면(참여자, 결과), 출력하지 않아도 게임의 결과를 알 수 있어요.

콘솔에 출력되는 사다리의 모양은 그저 사람이 결과를 보기 쉽게 알 수 있도록 해주는 문자열에 불과해요.

그렇다면 사다리를 출력하는 로직이 Line에 있는 게 올바를까요?

그저 자신의 상태를 View에 전달해서, 이를 출력하도록 하면 되지 않을까요?

for (Boolean point : points) {
System.out.print("|");
printLine(point);
}
System.out.println("|");
}

private void printLine(Boolean connected) {
if (connected) {
System.out.print("-----");
return;
}
System.out.print(" ");
}
}
18 changes: 18 additions & 0 deletions src/main/java/ladder/model/Name.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ladder.model;

public class Name {

private static final int MAX_LENGTH = 5;
private final String name;

public Name(String name) {
if (name.length() > MAX_LENGTH) {
throw new IllegalArgumentException("이름은 최대 5글자까지 가능합니다.");
}
this.name = name;
}

public boolean matches(String other) {
return name.equals(other);
}
}
32 changes: 32 additions & 0 deletions src/main/java/ladder/model/Participants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ladder.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Participants {

private final List<Name> names;

private Participants(List<Name> names) {
this.names = names;
}

public static Participants from(String input) {
return new Participants(
Arrays.stream(input.split(","))
.map(String::trim)
.map(Name::new)
.collect(Collectors.toList())
);
}
Comment on lines +16 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Participants를 생성하기 위한 정적 팩터리 메서드군요!

이 경우 입력된 문자열에 대해 ,를 기준으로 구분하여 Participants를 만들고 있는 것 같네요!

,는 View에 의존하는 값이고, 입력으로 들어온 문자열 또한 View에 의존되는 값이에요.

이 경우 도메인이 View를 직접적으로 의존하지는 않지만, 간접적으로 의존하는 것 같아요.


public int size() {
return names.size();
}

public List<Name> values() {
return new ArrayList<>(names);
}
Comment on lines +29 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Participants 클래스는 게임의 참여자를 의미하는 클래스 같네요!

그리고 values 메서드는 참여자의 이름을 모두 반환하도록 구현되어 있네요.

하지만 values 라는 이름을 보면, 참여자의 이름 목록이라는 의미가 명확하게 전달되지 않아 보여요.

}
16 changes: 16 additions & 0 deletions src/main/java/ladder/model/ResultType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ladder.model;

public enum ResultType {
ALL("all"),
SINGLE("");

private final String value;

ResultType(String value) {
this.value = value;
}

public static ResultType from(String input) {
return "all".equals(input) ? ALL : SINGLE;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 프로그래밍 요구사항
3항 연산자를 쓰지 않는다.

}
}
31 changes: 31 additions & 0 deletions src/main/java/ladder/model/Results.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ladder.model;

import java.util.ArrayList;
import java.util.List;

public class Results {

private final Participants participants;
private final List<String> results;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List results 게임 결과는 사용자에게 입력 받은 값을 그대로 출력해주는 역할 그 이외의 것을 하지 않아
일급컬렉션을 만드는 대신 List로 필드 선언을 했습니다. 일급 컬렉션으로 만드는 게 더 나은 선택이었을까요?
아직 기준이 모호한 듯 합니다.. ㅎㅎ

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급 컬렉션은 둘째치고, results가 가지는 List<String>이 어떤 의미를 나타내는지 의도를 전달하는 게 더 중요해 보여요.

이 경우 일급 컬렉션을 만들지 않았다면, List<Result>와 같이 타입을 명시적으로 나타내는 게 좋지 않았을까요?


public Results(Participants participants, List<String> results) {
this.participants = participants;
this.results = results;
}

public List<String> getAll() {
return new ArrayList<>(results);
}

public List<Name> getParticipants() {
return participants.values();
}

public String getResult(String participant) {
return participants.values().stream()
.filter(name -> name.matches(participant))
.findFirst()
.map(name -> results.get(participants.values().indexOf(name)))
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 참가자입니다."));
}
}
Loading