Skip to content
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

[1단계 - 사다리 생성] 몰리(김지민) 미션 제출합니다. #314

Merged
merged 45 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
09eff2d
docs(README.md): 기능 요구사항 정리
jminkkk Feb 20, 2024
c3611ac
test(PersonTest): 이름 검증 로직 테스트 작성
jminkkk Feb 20, 2024
fc9fbb3
feat(Person): 이름 검증 로직 작성
jminkkk Feb 20, 2024
6653ac1
test(LineTest): 라인 검증 로직 테스트 작성
jminkkk Feb 20, 2024
6674645
feat(Line): 라인 검증 로직 작성
jminkkk Feb 20, 2024
ad1df2e
test(PeopleTest): People 검증 테스트 작성
jminkkk Feb 20, 2024
eeb57ad
feat(People): People 검증 로직 작성
jminkkk Feb 20, 2024
8f7c474
test(LadderTest): Ladder 검증 테스트 작성
jminkkk Feb 20, 2024
233b959
feat(Ladder): Ladder 검증 로직 작성
jminkkk Feb 20, 2024
62b7858
test(LineTest): Line의 경로가 연달아 있지 않은지 검증하는 테스트 작성
jminkkk Feb 20, 2024
e4b4705
feat(Line): 연달아 있지 않은 Line의 경로를 생성하는 로직 작성
jminkkk Feb 20, 2024
38c0034
test(NameParserTest): 입력받은 이름을 파싱하는 테스트 작성
jminkkk Feb 20, 2024
8611540
feat(NameParser): 입력받은 이름을 파싱하는 로직 작성
jminkkk Feb 21, 2024
b686b45
feat(InputView): 높이를 입력받는 로직 작성
jminkkk Feb 21, 2024
e3374fe
test(NameFormatterTest): 형식에 맞춰 이름을 반환하는 테스트 작성
jminkkk Feb 21, 2024
6248314
feat(NameFormatter): 형식에 맞춰 이름을 반환하는 로직 작성
jminkkk Feb 21, 2024
4dd6267
test(LineFormatterTest): 형식에 맞춰 라인을 반환하는 기능 테스트 작성
jminkkk Feb 21, 2024
f3c1f14
feat(LineFormatter): 형식에 맞춰 라인을 반환하는 기능 작성
jminkkk Feb 21, 2024
77dcea7
test(NamesFormatterTest): 형식에 맞춰 이름들을 반환하는 테스트 작성
jminkkk Feb 21, 2024
75a2038
feat(NamesFormatter): 형식에 맞춰 이름들을 반환하는 로직 작성
jminkkk Feb 21, 2024
7332308
feat(OutputView): 형식에 맞춰 실행결과를 출력하는 로직 작성
jminkkk Feb 21, 2024
ef5a04a
feat(InputView): 입력 문구 출력 추가
jminkkk Feb 21, 2024
8c89ecc
feat(Ladder, LadderTest): 사다리의 정적 팩토리 메서드에 인원수 인자 추가
jminkkk Feb 21, 2024
007b784
refactor(LineTest): 연결 여부 변수명 교체
jminkkk Feb 21, 2024
64f266e
feat(Result, LineDto): 출력에 필요한 Dto 추가
jminkkk Feb 21, 2024
19da1be
feat(LadderGame, Application): 컨트롤러 작성 및 메인 메서드 추가
jminkkk Feb 21, 2024
fe1bc1f
refactor(LineTest): 경로 존재 여부 원시값 포장 테스트 작성
jminkkk Feb 22, 2024
a023ce8
refactor(Line, Path, LineInfo): 경로 존재 여부 원시값 포장
jminkkk Feb 22, 2024
8bf415d
refactor(LineTest): 연달아 있는 사다리 테스트 로직 수정
jminkkk Feb 22, 2024
c683dd0
style(PeopleTest, PersonTest, NamesFormatterTest): 필요없는 줄 제거
jminkkk Feb 22, 2024
958d538
refactor(LineTest): 라인의 생성자로 값 리스트를 받도록 변경
jminkkk Feb 22, 2024
a80f45a
refactor(Line): 라인의 생성자 변경, 검증로직 추가
jminkkk Feb 22, 2024
64a2af2
refactor(LadderTest): 사다리 정적 팩토리 메서드 인자 변경
jminkkk Feb 22, 2024
7460b56
refactor(src): 메서드 및 변수에 final 키워드 추가
jminkkk Feb 22, 2024
b9ca5e0
style(NameParserTest): 파일 끝에 개행 추가
jminkkk Feb 25, 2024
9f73c3e
refactor(formatter): formatter의 패키지 위치를 view 아래로 이동
jminkkk Feb 25, 2024
3897b47
refactor(Ladder): pathCount 변수 추출
jminkkk Feb 25, 2024
d815880
refactor(Line): 인원 수 검증이 아닌 경로의 사이즈로 검증
jminkkk Feb 25, 2024
1b21725
fix(Line): 경로 검증 시 depth를 1으로 줄이기 위해 메서드 추출
jminkkk Feb 25, 2024
164e846
refactor(parser): 패키지 위치를 view 아래로 이동
jminkkk Feb 25, 2024
a178d6a
test(LadderTest): 사다리 생성에 대한 테스트 추가
jminkkk Feb 25, 2024
9ceeb00
feat(FixedPathGenerator): 테스트를 위한 PathGenerator 구현체 생성
jminkkk Feb 25, 2024
6546037
feat(Line): equals 메서드 구현
jminkkk Feb 25, 2024
ddcab56
feat(Application): 예외 처리 추가
jminkkk Feb 25, 2024
76f9182
style(PersonTest): 파일 끝에 개행 추가
jminkkk Feb 25, 2024
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
19 changes: 19 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### 기능 요구사항 정리
Copy link

Choose a reason for hiding this comment

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

깔끔한 요구사항 정리 👍

- [x] 사람의 이름을 갖는다.
- [x] 이름은 최소 1글자 최대 5글자다.
- [x] 라인은 사다리의 가로 한줄을 의미한다.
- [x] 한 라인은 사람수보다 하나 적은 폭을 갖는다.
- [x] 사다리의 경로(path:가로라인)은 연달아 있을 수 없다. (나란히 있을 수 없다.)
- [x] 참여할 사람의 이름은 쉼표로 구분해 받는다.
- [x] 사람은 최소 2명 참가해야한다.
- [x] 사다리의 높이를 입력받는다.
- [x] 최소 높이는 1 이상이다.
- [x] 실행 결과를 출력한다.
- [x] 사다리를 출력한다.
- [x] 라인의 첫 부분은 ` ` 4칸으로 출력한다.
- [x] 5칸과 가장 오른쪽에 `|`를 높이만큼 출력한다.
- [x] 라인에 경로가 있는 경우 `-`를 5칸을 출력한다.
- [x] 라인에 경로가 없는 경우 ` `를 5칸을 출력한다.
- [x] 이름을 출력한다.
- [x] 이름이 4글자 이하인 경우, 오른쪽에 하나의 공백을 두고 우측 정렬한다.
- [x] 이름이 5글자인 경우, 공백없이 우측 정렬한다.
14 changes: 14 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import controller.LadderGame;
import view.InputView;
import view.OutputView;

public class Application {
public static void main(String[] args) {
try {
final LadderGame ladderGame = new LadderGame(new InputView(), new OutputView());
ladderGame.play();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
38 changes: 38 additions & 0 deletions src/main/java/controller/LadderGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package controller;

import dto.Result;
import java.util.List;
import model.Ladder;
import model.People;
import model.path.RandomPathGenerator;
import view.InputView;
import view.OutputView;

public class LadderGame {
private final InputView inputView;
private final OutputView outputView;

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

public void play() {
final People people = initPeople();
final Ladder ladder = initLadder(people.getPersonCount());

final Result result = Result.from(people, ladder);
outputView.printResult(result);
}


private People initPeople() {
final List<String> names = inputView.inputNames();
return People.from(names);
}

private Ladder initLadder(final int personCount) {
final int height = inputView.inputHeight();
return Ladder.from(height, personCount, new RandomPathGenerator());
}
}
11 changes: 11 additions & 0 deletions src/main/java/dto/LineInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dto;

import java.util.List;
import model.Line;

public record LineInfo(List<Boolean> lineInfo) {
public static LineInfo from(final Line line) {
final List<Boolean> lineInfo = line.getExistFlags();
return new LineInfo(lineInfo);
}
}
17 changes: 17 additions & 0 deletions src/main/java/dto/Result.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dto;

import java.util.List;
import model.Ladder;
import model.People;

public record Result(List<String> names, List<LineInfo> lines) {
Copy link

Choose a reason for hiding this comment

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

record 활용 👍

public static Result from(final People people, final Ladder ladder) {
final List<String> names = people.getNames();
final List<LineInfo> lines = ladder.getLines()
.stream()
.map(LineInfo::from)
.toList();

return new Result(names, lines);
}
}
34 changes: 34 additions & 0 deletions src/main/java/model/Ladder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package model;

import java.util.ArrayList;
import java.util.List;
import model.path.PathGenerator;

public class Ladder {
private final List<Line> lines;

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

public static Ladder from(final int height, final int personCount, final PathGenerator pathGenerator) {
validateHeight(height);
final List<Line> lines = new ArrayList<>();
for (int i = 0; i < height; i++) {
int pathCount = personCount - 1;
final Line line = new Line(pathGenerator.generate(pathCount));
lines.add(line);
}
return new Ladder(lines);
}

private static void validateHeight(final int height) {
if (height < 1) {
throw new IllegalArgumentException("사다리의 높이는 1 이상이어야 합니다.");
}
}

public List<Line> getLines() {
return lines;
}
}
55 changes: 55 additions & 0 deletions src/main/java/model/Line.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package model;

import java.util.List;
import java.util.Objects;
import model.path.Path;

public class Line {
private static final int MIN_PATH_SIZE = 1;

private final List<Path> paths;

public Line(final List<Path> paths) {
validatePathCount(paths.size());
checkContinuousPaths(paths);
this.paths = paths;
}

private void validatePathCount(final int pathSize) {
if (pathSize < MIN_PATH_SIZE) {
throw new IllegalArgumentException("사다리의 경로는 비어있더라도 최소 1개 이상이여야 합니다.");
}
}

private void checkContinuousPaths(final List<Path> paths) {
for (int i = 0; i < paths.size() - 1; i++) {
final Path left = paths.get(i);
final Path right = paths.get(i + 1);
validateContinuousPaths(left, right);
}
}

private void validateContinuousPaths(final Path left, final Path right) {
if (left == Path.EXIST && right == Path.EXIST) {
throw new IllegalArgumentException("사다리의 경로는 연달아 있을 수 없습니다.");
}
}

public List<Boolean> getExistFlags() {
return paths.stream()
.map(Path::isExist)
.toList();
}

@Override
public boolean equals(final Object o) {
Copy link

Choose a reason for hiding this comment

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

Line에서 equals 를 재정의 해주신 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

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

Ladder 테스트에서 생성한 Ladder의 Line이 주입한 Line과 동일한지를 통해 Ladder가 잘 생성되는지를 검증하려고 추가를 했습니다...ㅎㅎ
답변을 적다보니equals를 사용하지 않고 구현할 수 있을 것 같네요.. 수정하겠습니다..!

if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Line line = (Line) o;
return Objects.equals(paths, line.paths);
}
}
37 changes: 37 additions & 0 deletions src/main/java/model/People.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package model;

import java.util.List;

public class People {
Copy link

Choose a reason for hiding this comment

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

일급 컬렉션 구현 👍

private static final int MIN_PERSON_COUNT = 2;

private final List<Person> personGroup;

private People(final List<Person> personGroup) {
validatePersonCount(personGroup.size());
this.personGroup = personGroup;
}

private void validatePersonCount(final int personCount) {
if (personCount < MIN_PERSON_COUNT) {
throw new IllegalArgumentException("사람은 최소 2명 참가해야 합니다.");
}
}

public static People from(final List<String> names) {
final List<Person> personGroup = names.stream()
.map(Person::new)
.toList();
return new People(personGroup);
}

public List<String> getNames() {
return personGroup.stream()
.map(Person::getName)
.toList();
}

public int getPersonCount() {
return personGroup.size();
}
}
24 changes: 24 additions & 0 deletions src/main/java/model/Person.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package model;

public class Person {
private static final int MIN_NAME_LENGTH = 1;
private static final int MAX_NAME_LENGTH = 5;

private final String name;

public Person(final String name) {
validateNameLength(name);
this.name = name;
}

private void validateNameLength(final String name) {
final int length = name.length();
if (length < MIN_NAME_LENGTH || length > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("이름은 최소 1글자 최대 5글자여야 합니다.");
}
}

public String getName() {
return name;
}
}
16 changes: 16 additions & 0 deletions src/main/java/model/path/FixedPathGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package model.path;

import java.util.List;

public class FixedPathGenerator implements PathGenerator {
Copy link

Choose a reason for hiding this comment

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

테스트를 위한 객체는 테스트가 있는 패키지로 이동시켜도 되겠어요!
-> 다른 개발자에게 혼란을 줄 수 있으니까요 😃

private final List<Path> lines;

public FixedPathGenerator(final List<Path> lines) {
this.lines = lines;
}

@Override
public List<Path> generate(final int count) {
return this.lines;
}
}
16 changes: 16 additions & 0 deletions src/main/java/model/path/Path.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package model.path;

public enum Path {
Copy link

Choose a reason for hiding this comment

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

사다리 경로를 enum으로 표현 👍 👍 👍 👍

EXIST(true),
NOT_EXIST(false);

private final boolean isExist;

Path(final boolean isExist) {
this.isExist = isExist;
}

public boolean isExist() {
return isExist;
}
}
7 changes: 7 additions & 0 deletions src/main/java/model/path/PathGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package model.path;

import java.util.List;

public interface PathGenerator {
List<Path> generate(final int count);
Copy link

Choose a reason for hiding this comment

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

경로 생성은 interface로 추상화 👍

어떤 의도로 추상화를 해주셨는지 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

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

[추상화 시도 이유]

처음 테스트 코드를 작성할 때는,
Line 클래스 내부에서 Random을 사용하여, 랜덤한 List<Boolean> (= 지금의 List<Path>)에서 true가 연달아 나오는가? 를 테스트했는데요!

        // 수정 전의 LineTest.java
        Line line = new Line(personCount);
        for (int i = 0; i < personCount - 1; i++) {
            assertThat(line.get(i)).isNotEqualTo(line.get(i + 1));
        }

하지만 위 테스트는 제어할 수 없는 Random 때문에 신뢰할 수 없는 테스트라는 생각이 들었습니다...!

때문에 Line에는 List를 주입받고 Ladder에서 전달을 해야겠네? -> 그러면 Ladder에서 Random을 사용하는 건 괜찮을까? -> 인터페이스를 받아서 상황에 따른 구현체를 사용하자

라는 생각의 흐름으로 나아갔던 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

[추가 궁금한 점 - RandomPathGenerator의 생성시점]

그런데 위 생각을 작성하다가 궁금한 점이 생겼어요. 찰리!
현재는 controller 패키지 안의 LadderGame에서 RandomPathGenerator를 생성하는데요!

    // LadderGame.java
    private Ladder initLadder(final int personCount) {
        final int height = inputView.inputHeight();
        return Ladder.from(height, personCount, new RandomPathGenerator()); // RandomPathGenerator 생성
    }

RandomPathGenerator의 생성 시점은 언제가 더 적절할 지에 대한 찰리의 의견이 궁금합니다!

저는 일단 2가지의 시점을 생각해보았어요!

  1. LadderGame 생성 시
  2. LadderGame 내부의 Ladder 생성 시(현재 방법)

1번 방법은,
장점으로 현재는 테스트하기 어려운 LadderGame 자체를 테스트 해볼 수 있다는 생각이 들었고, 단점으로 LadderGame 생성 시 PathGenerator까지 생성해주어야 한다는 사실을 다른 개발자가 이해하기 힘들 수도 있고, PathGenerator를 실행부까지 노출 시켜도 되는 것인가 하는 생각이 들었어요.

제가 현재 2번 방법을 선택한 이유는
단위 테스트를 통해 핵심 모델에 대해 테스트를 진행했고 때문에 LadderGame를 테스트해야 할 필요성을 느끼지 못했었어요.
또 현재 LadderGame가 컨트롤러의 역할을 하는데, LadderGame보다는 모델인 Ladder의 생성 시점에 RandomPathGenerator를 전달하는 것이 자연스러울 것 같았기 때문이에요..!

찰리의 의견이 궁금합니다..!

Copy link

Choose a reason for hiding this comment

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

Line 클래스 내부에서 Random을 사용하여, 랜덤한 List (= 지금의 List)에서 true가 연달아 나오는가? 를 테스트했는데요!
하지만 위 테스트는 제어할 수 없는 Random 때문에 신뢰할 수 없는 테스트라는 생각이 들었습니다...!

테스트를 작성하고 고민해보니 더 좋은 방향으로 리팩토링된 케이스군요 👍

이 부분도 테스트의 장점이라고 생각해요! 더 나은 구조로 변경하게 해주는 것 😃
도메인 내에 제어할 수 있는 부분만 남았으니 코드를 이해하는데 훨씬 간결해진다고 생각해요!

때문에 Line에는 List를 주입받고 Ladder에서 전달을 해야겠네? -> 그러면 Ladder에서 Random을 사용하는 건 괜찮을까? -> 인터페이스를 받아서 상황에 따른 구현체를 사용하자

생각의 흐름이 훌륭하네요 💯 💯 💯 💯

Copy link

Choose a reason for hiding this comment

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

중요한 고민이라고 생각해요!! 👍

각각의 방법에서 생각하는 장점도 말씀해주셔서 감사해요 🙇

  1. LadderGame이 생성되는 시점
    • 생성자에서 인자로 받거나 생성자 내부에서 RandomPathGenerator를 생성하는 방법이 떠오르네요 :)
    • 이렇게 했을 때 장점은
      • 말씀대로 LadderGame을 테스트하는데 Random 요소를 제어할 수 있고
      • LadderGame에 다양한 PathGenerator 방법을 주입해줄 수 있곘네요 :)
    • 말씀해주신 단점은
      • PathGenerator 생성 여부가 알기 힘들다는 생성자에서 PathGenerator를 인자로 받는다면 명확하게 알려줄 수 있는 부분이겠네요 :)
      • 실행부(Application) 까지 model이 노출되어도 되는가? 는 고민해야하는 부분이네요! 🤔 몰리는 어떻게 생각하시나요?
  2. LadderGame 내부의 Ladder 생성 시점

단위 테스트를 통해 핵심 모델에 대해 테스트를 진행했고 때문에 LadderGame를 테스트해야 할 필요성을 느끼지 못했었어요.

동의하는 부분입니다 👍 저도 LadderGame을 테스트했을 때 얻는 장점이 커보이지 않아요

현재 LadderGame가 컨트롤러의 역할을 하는데, LadderGame보다는 모델인 Ladder의 생성 시점에 RandomPathGenerator를 전달하는 것이 자연스러울 것 같았기 때문이에요..!

좋은 전략이라 생각해요 👍
Ladder를 생성하기 위해 어떤 방법을 쓰는지 코드상에서 명확하네요!

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

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

public class RandomPathGenerator implements PathGenerator {
private final Random random = new Random();

@Override
public List<Path> generate(final int count) {
final List<Path> paths = new ArrayList<>();
while (paths.size() < count) {
Path randomPath = getNextPath(paths);
paths.add(randomPath);
}
return paths;
}

private Path getNextPath(final List<Path> paths) {
if (!paths.isEmpty() && getLastPath(paths) == Path.EXIST) {
Copy link

Choose a reason for hiding this comment

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

아래와 같이 메서드를 추출하면 한눈에 알 수 있을것같아요
물론 지금도 읽기에 무리는 없어보이네요 ㅎㅎ.... 넘어가셔도됩니다

Suggested change
if (!paths.isEmpty() && getLastPath(paths) == Path.EXIST) {
if (!paths.isEmpty() && this.existLastPath(paths)) {

return Path.NOT_EXIST;
}
if (random.nextBoolean()) {
return Path.EXIST;
}
return Path.NOT_EXIST;
}

private Path getLastPath(final List<Path> paths) {
return paths.get(paths.size() - 1);
}

}
21 changes: 21 additions & 0 deletions src/main/java/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package view;

import java.util.List;
import java.util.Scanner;
import view.parser.NameParser;

public class InputView {
private final Scanner scanner = new Scanner(System.in);

public List<String> inputNames() {
System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)");
final String text = scanner.next();
return NameParser.parse(text);
}

public int inputHeight() {
System.out.println();
System.out.println("최대 사다리 높이는 몇 개인가요?");
return scanner.nextInt();
}
}
32 changes: 32 additions & 0 deletions src/main/java/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package view;

import dto.LineInfo;
import dto.Result;
import view.formatter.LineFormatter;
import view.formatter.NamesFormatter;
import java.util.List;

public class OutputView {

public void printResult(final Result result) {
System.out.println();
System.out.println("실행결과");
System.out.println();
final List<String> names = result.names();
printNames(names);
printLines(result.lines());
}

public void printNames(final List<String> names) {
System.out.println(NamesFormatter.format(names));
}

public void printLines(final List<LineInfo> lines) {
lines.forEach(this::printLine);
}

private void printLine(final LineInfo line) {
final List<Boolean> paths = line.lineInfo();
System.out.println(LineFormatter.format(paths));
}
}
Loading