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단계 - 사다리 생성] 페드로(류형욱) 미션 제출합니다 #286

Merged
merged 50 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c41161d
update .gitignore
BurningFalls Feb 20, 2024
a12491d
feat(player): 사용자 이름 길이를 검증하는 기능 구현
BurningFalls Feb 20, 2024
1e98f6e
feat(player): 이름 형식을 검증하는 기능 구현
BurningFalls Feb 20, 2024
68009aa
feat(ladderHeight): 사다리 높이가 자연수인지 검증하는 기능 구현
BurningFalls Feb 20, 2024
3f5c32f
feat(line): 왼쪽에 사다리가 있는지 확인하는 기능 구현
BurningFalls Feb 20, 2024
acdc850
feat(line): 특정 길이의 한 행을 생성하는 기능 구현
BurningFalls Feb 20, 2024
36fe94d
feat(line): 한 행의 왼쪽에 경로가 있는지 확인하는 기능 구현
BurningFalls Feb 20, 2024
08955b2
feat(ladder): 지정된 높이와 폭의 사다리를 생성하는 기능 구현
BurningFalls Feb 20, 2024
02394eb
feat(line): 연속된 경로 없이 가로줄을 생성하는 기능 구현
BurningFalls Feb 21, 2024
f24d5f2
refactor(line): 경로 생성 로직 메서드 추출
BurningFalls Feb 21, 2024
9035f45
docs(readme): 랜덤 생성 위치 변경에 따른 요구사항 명세서 수정
BurningFalls Feb 21, 2024
8894f33
feat(line): 유효한 가로줄인지 검증하는 기능 구현
BurningFalls Feb 21, 2024
f2a6ebd
feat(ladder): 지정된 높이와 폭의 사다리를 생성하는 기능 구현
BurningFalls Feb 21, 2024
c3db653
feat(ladder): 유효한 가로줄로 사다리를 구성하는 기능 구현
BurningFalls Feb 21, 2024
ab0e879
remove(/utils): 미 사용 패키지 제거
BurningFalls Feb 21, 2024
7a15b28
feat(inputview): 사용자로부터 이름을 입력받는 기능 구현
BurningFalls Feb 21, 2024
4a32498
feat(inputView): 사용자로부터 사다리 높이를 입력받는 기능 구현
BurningFalls Feb 21, 2024
02a9cba
feat(outputView): 참여자 이름 출력 기능 구현
BurningFalls Feb 21, 2024
6a0179f
feat(outputView): 사다리 출력 기능 구현
BurningFalls Feb 21, 2024
57e7d3e
refactor(ladder): 랜덤 경로 생성 메서드의 반환 형태 변경
BurningFalls Feb 22, 2024
db70eab
refactor(ladder): 변수 인라인화
BurningFalls Feb 22, 2024
989beb3
refactor(line): LadderPath 패턴 검증 로직의 순회 구조 변경
BurningFalls Feb 22, 2024
4749c0f
refactor(ladderSize): 클래스 레코드화
BurningFalls Feb 22, 2024
9229cc5
refactor(lineStringFormatter): 상수 포장
BurningFalls Feb 22, 2024
8100bf3
refactor(ladderController): 사용자 입력을 담당하는 메서드 분리
BurningFalls Feb 22, 2024
88cbcff
refactor(lineDto): DTO 변환 과정에서 마지막 요소 삭제 로직 개선
BurningFalls Feb 22, 2024
1e77b7b
refactor(line): row 반환 시 unmodifiableList를 반환하도록 변경
BurningFalls Feb 22, 2024
2559203
fix(ladder): 잘못 지정된 접근제어자 수정
BurningFalls Feb 22, 2024
bf82fec
feat(Players): 중복된 이름이 존재하는지 확인하는 기능 구현
BurningFalls Feb 22, 2024
0b81f89
refactor(ladderController): 컨트롤러의 public 메서드를 하나로 통합
hw0603 Feb 26, 2024
615a256
refactor(ladderPath): LadderPath enum 패키지 변경
hw0603 Feb 26, 2024
ebc2132
fix(lineDto): 누락된 접근 제어자 추가
hw0603 Feb 26, 2024
1740b58
refactor(lineDto): Line을 LineDto로 변환하는 로직의 위치 변경
hw0603 Feb 26, 2024
cfed9fa
refactor(ladder): 지역변수 초기화 위치 변경
hw0603 Feb 26, 2024
612c913
refactor(ladder): 랜덤 행 생성 메서드 본문을 분리하여 의도를 명확하게 드러내도록 변경
hw0603 Feb 26, 2024
c8d4463
refactor(ladder): random.nextBoolean() 호출부에서 의미 명확화
hw0603 Feb 26, 2024
0924132
refactor(ladder): 정적 팩토리 메서드에서 스트림을 사용하도록 변경
hw0603 Feb 26, 2024
26a1203
refactor(ladderSize): LadderSize 를 제거하고 Ladder에서 크기를 관리하도록 변경
hw0603 Feb 26, 2024
424085b
refactor(inputView): 이름 구분자 상수화
hw0603 Feb 26, 2024
434aa96
refactor(/view): view 패키지 클래스들의 인스턴스화 방지
hw0603 Feb 26, 2024
1b67f2a
refactor(outputView): LineStringFormatter를 OutputView로 통합
hw0603 Feb 26, 2024
0cb3c19
fix(outputView): createLineString() 의 접근제어자 변경
hw0603 Feb 26, 2024
684c0c9
style(outputView): for-each 문 축약어 삭제
hw0603 Feb 26, 2024
4cfd531
refactor(/test): Test 클래스들의 public 접근지정자 삭제
hw0603 Feb 26, 2024
4d592b4
refactor(ladderTest): @ParameterizedTest 적용
hw0603 Feb 26, 2024
79907af
test(playersTest): 정상적으로 객체가 생성되는 상황에 대한 테스트 코드 추가
hw0603 Feb 26, 2024
d0f7e3c
style(line): 메서드명에서 LR, RL과 같은 축약어 삭제
hw0603 Feb 26, 2024
42cd6de
fix(lineTest): fix typo
hw0603 Feb 26, 2024
3946292
style(line): 메서드 이름 변경
hw0603 Feb 26, 2024
e916985
refactor(consoleReader): IOException 을 외부로 던지지 않고 ConsoleReader에서 처리하…
hw0603 Feb 27, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ out/

### VS Code ###
.vscode/

.gitmessage.txt
41 changes: 41 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## 주요 기능

1. 참여할 사람 이름 입력
2. 사다리 높이 입력
3. 이름과 높이로 사다리 생성
4. 실행 결과 출력

### 참여할 사람 이름 입력
- [x] 이름 입력 안내 문구 출력
- [x] 사용자로부터 이름을 입력받는 기능
- [x] `,` 를 기준으로 이름을 분할하는 기능
- [x] 중복된 이름이 존재하는지 확인하는 기능

### 사다리 높이 입력
- [x] 높이 입력 안내 문구 출력
- [x] 사용자로부터 사다리 높이를 입력받는 기능

### 이름과 높이로 사다리 생성
- [x] 지정된 높이와 폭의 사다리를 생성하는 기능
- [x] 특정 위치에서 경로의 연결 여부를 랜덤하게 선택하는 기능
- [x] 생성된 유효한 가로줄로 사다리를 구성하는 기능

### 가로줄 생성
- [x] 유효한 가로줄인지 검증하는 기능
- 한 가로줄 내에 연속된 경로가 없어야 한다.
- 오른쪽 경로 오른쪽에 항상 왼쪽 경로가 있어야 한다.
- 왼쪽 경로 왼쪽에 항상 오른쪽 경로가 있어야 한다.

### 실행 결과 출력

- [x] “실행 결과” 문구 출력
- [x] 참여자 이름 출력
- [x] 사다리 출력

### 예외 처리

- [x] 유효한 참여자 이름이 아닐 경우 예외 발생
- 이름의 길이는 1 이상 5이하여야 함
- 이름에 포함되는 문자는 영문자와 숫자만 허용함
- [x] 유효한 사다리 높이가 아닐 경우 예외 발생
- 높이는 항상 자연수여야 함
10 changes: 10 additions & 0 deletions src/main/java/ladder/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ladder;

import ladder.controller.LadderController;

public class Application {
public static void main(String[] args) {
LadderController ladderController = new LadderController();
ladderController.start();
}
}
40 changes: 40 additions & 0 deletions src/main/java/ladder/controller/LadderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ladder.controller;

import ladder.model.Ladder;
import ladder.model.Players;
import ladder.view.InputView;
import ladder.view.OutputView;

import java.util.List;

public class LadderController {
private Players ladderPlayers;
private Ladder ladder;

public void start() {
init();
printResult();
}

private void init() {
ladderPlayers = Players.from(readPlayerNames());

int height = readLadderHeight();
int width = ladderPlayers.getSize();
ladder = Ladder.of(height, width);
}

private List<String> readPlayerNames() {
return InputView.inputPlayerNames();
}

private int readLadderHeight() {
return InputView.inputLadderHeight();
}

private void printResult() {
OutputView.printResultDescription();
OutputView.printPlayerNames(ladderPlayers.getPlayerNames());
OutputView.printLadder(ladder.toLineDtoList());
}
}
21 changes: 21 additions & 0 deletions src/main/java/ladder/dto/LineDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ladder.dto;

import ladder.model.Line;

import java.util.List;

public class LineDto {
private final List<Boolean> connected;

private LineDto(List<Boolean> connected) {
this.connected = connected;
}

public static LineDto from(Line line) {
return new LineDto(line.getConnected());
}

public List<Boolean> getConnected() {
return connected;
}
}
72 changes: 72 additions & 0 deletions src/main/java/ladder/model/Ladder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ladder.model;

import java.util.stream.IntStream;
import ladder.dto.LineDto;

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

public class Ladder {
private static final Random random = new Random();
Copy link
Member

Choose a reason for hiding this comment

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

프로젝트 전반에 걸쳐 상수 네이밍 컨벤션을 전체 대문자로 해보는건 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

오... 한번도 상수화된 인스턴스를 상수라고 생각해 본 적이 없었는데 생각해보니 상수가 아닐 이유가 없네요.

상수 변수에 대해서는 전체 대문자 네이밍 컨벤션을 지키는 편인데, 이와 같은 경우는 사람마다 의견이 갈리는 것 같아요.

https://stackoverflow.com/questions/7259687/java-naming-convention-for-static-final-variables/7259738

로거를 LOGGER로 쓸지 logger로 쓸지 결정하는 것이 비슷한 상황일 것 같은데, 저는 소문자가 편한 것 같아요!

피케이는 static final 로 선언된 인스턴스 변수들도 모두 uppercase로 작성하시나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

크루들과 이야기해보다가 이런 글을 발견했어요!
https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names

// Constants
static final int NUMBER = 5;
static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
static final Map<String, Integer> AGES = ImmutableMap.of("Ed", 35, "Ann", 32);
static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutable
static final SomeMutableType[] EMPTY_ARRAY = {};

// Not constants
static String nonFinal = "non-final";
final String nonStatic = "non-static";
static final Set<String> mutableCollection = new HashSet<String>();
static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);
static final ImmutableMap<String, SomeMutableType> mutableValues =
    ImmutableMap.of("Ed", mutableInstance, "Ann", mutableInstance2);
static final Logger logger = Logger.getLogger(MyClass.getName());
static final String[] nonEmptyArray = {"these", "can", "change"};

private final List<Line> ladder;

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

public static Ladder of(int height, int width) {
validate(height, width);

List<Line> ladder = IntStream.range(0, height)
.mapToObj(idx -> new Line(makeRandomRow(width)))
.toList();

return new Ladder(ladder);
}

private static void validate(int height, int width) {
if (notNaturalNumber(height) || notNaturalNumber(width)) {
throw new IllegalArgumentException("사다리 높이와 너비는 자연수여야 합니다.");
}
}

private static boolean notNaturalNumber(int value) {
return value <= 0;
}

private static List<LadderPath> makeRandomRow(int width) {
List<LadderPath> randomPath = new ArrayList<>(generatePairableRandomPath(width));

if (randomPath.size() < width) {
randomPath.add(LadderPath.STAY);
}

return randomPath;
}

private static List<LadderPath> generatePairableRandomPath(int maxWidth) {
List<LadderPath> randomPathWithPair = new ArrayList<>();

while (randomPathWithPair.size() < maxWidth - 1) {
randomPathWithPair.addAll(generateRandomPath());
}

return randomPathWithPair;
}

private static List<LadderPath> generateRandomPath() {
boolean isConnectedPath = random.nextBoolean();

if (isConnectedPath) {
return List.of(LadderPath.RIGHT, LadderPath.LEFT);
}
return List.of(LadderPath.STAY);
}

public List<LineDto> toLineDtoList() {
return ladder.stream()
.map(LineDto::from)
.toList();
}
Comment on lines +67 to +71
Copy link
Member

Choose a reason for hiding this comment

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

Line.toLineDto() 를 사용하기를 지양하셨듯, List 를 만들어주는 로직을 Ladder 가 아닌 다른 곳에 넣어보면 어떨까요? 도메인은 최대한 다른 클래스에 의존성을 가지지 않는 것이 좋아요. 도메인을 '외부 요소의 변경'으로부터 최대한 지키는거죠.

Copy link
Member Author

Choose a reason for hiding this comment

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

반영했습니다! 이 변경과 관련하여 궁금한 점이 생겼는데, 2단계 PR때 정리해서 질문드릴게요!

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

public enum LadderPath {
STAY, LEFT, RIGHT;
}
56 changes: 56 additions & 0 deletions src/main/java/ladder/model/Line.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ladder.model;

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

import static ladder.model.LadderPath.*;

public class Line {
private final List<LadderPath> row;

public Line(List<LadderPath> row) {
validate(row);
this.row = row;
}

private void validate(List<LadderPath> row) {
if (isLeftOnFirst(row) || isRightOnEnd(row)) {
throw new IllegalArgumentException("유효한 가로줄이 아닙니다.");
}
if (!isLeftAlwaysExistAfterRight(row) || !isRightAlwaysExistBeforeLeft(row)) {
throw new IllegalArgumentException("유효한 가로줄이 아닙니다.");
}
}

private boolean isLeftOnFirst(List<LadderPath> row) {
return row.get(0) == LEFT;
}

private boolean isRightOnEnd(List<LadderPath> row) {
return row.get(row.size() - 1) == RIGHT;
}

private boolean isLeftAlwaysExistAfterRight(List<LadderPath> row) {
return IntStream.range(0, row.size())
.filter(idx -> row.get(idx).equals(RIGHT))
.map(idx -> idx + 1)
.allMatch(idx -> row.get(idx).equals(LEFT));
}

private boolean isRightAlwaysExistBeforeLeft(List<LadderPath> row) {
return IntStream.range(0, row.size())
.filter(idx -> row.get(idx).equals(LEFT))
.map(idx -> idx - 1)
.allMatch(idx -> row.get(idx).equals(RIGHT));
}

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

public List<Boolean> getConnected() {
return IntStream.range(0, row.size() - 1)
.mapToObj(idx -> row.get(idx).equals(RIGHT))
.toList();
}
}
41 changes: 41 additions & 0 deletions src/main/java/ladder/model/Player.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ladder.model;

public class Player {
private static final int MAX_NAME_LENGTH = 5;
private static final String NAME_PATTERN = "^[a-zA-Z0-9]*$";

private final String name;

public Player(String name) {
validate(name);
this.name = name;
}

private void validate(String name) {
if (isNameLengthLongerThanMaxLength(name)) {
throw new IllegalArgumentException("이름의 길이는 5를 초과할 수 없다.");
}
if (isNameEmpty(name)) {
throw new IllegalArgumentException("이름이 비어 있습니다.");
}
if (!isNameFormatValid(name)) {
throw new IllegalArgumentException("이름은 영문자와 숫자로 구성되어야 합니다.");
}
}

private boolean isNameLengthLongerThanMaxLength(String name) {
return name.length() > MAX_NAME_LENGTH;
}

private boolean isNameEmpty(String name) {
return name.isEmpty();
}

private boolean isNameFormatValid(String name) {
return name.matches(NAME_PATTERN);
}

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

import java.util.HashSet;
import java.util.List;

public class Players {
private final List<Player> players;

private Players(List<Player> players) {
this.players = players;
}

public static Players from(List<String> playerNames) {
validate(playerNames);
return new Players(playerNames.stream()
.map(Player::new)
.toList()
);
}
Comment on lines +13 to +19
Copy link
Member

Choose a reason for hiding this comment

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

Q) 정적팩토리 메서드에서 메서드 분리를 통해 한 번에 하나의 조건만 검증하도록 했더니 static 메서드가 너무 많아지는 문제가 있었습니다.

정상이에요 괜찮아요. 이 정도 static 으로 문제가 발생할정도로 서버가 나약하지 않아요 :)
정적팩토리 메서드의 이점, 메서드 분리의 이점을 챙기기 위해 static 메서드 수가 늘어난 것이니 give-and-take 라고 볼 수도 있구요.

만약 그럼에도 static 이 늘어나는걸 원하지 않으신다면, 이름을 Player 로 바꾸는 과정을 외부에서 진행하고 생성자를 통해서 Players 를 만들어도 되겠네요! 오히려 정적 팩토리 메서드를 사용하는 것을 지양하는거죠. 이것 역시도 내가 무언가를 지향하는 기준을 세운다면, 반대로 다른 것을 포기해야해요. 자신의 상황에 맞게 이런 정답이 없는 고민을 하고 결정을 내려야해서 프로그래밍이 어렵나봐요. 🥲

Copy link
Member Author

Choose a reason for hiding this comment

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

찝찝했던 부분이 시원하게 해결되었습니다!ㅎㅎ 설계는 트레이드오프 활동이라는게 정말 맞는 것 같아요.


private static void validate(List<String> playerNames) {
if (isDuplicated(playerNames)) {
throw new IllegalArgumentException("중복되는 이름이 존재합니다.");
}
}

private static boolean isDuplicated(List<String> playerNames) {
return new HashSet<>(playerNames).size() != playerNames.size();
}

public int getSize() {
return players.size();
}

public List<String> getPlayerNames() {
return players.stream()
.map(Player::getName)
.toList();
}
}
20 changes: 20 additions & 0 deletions src/main/java/ladder/utils/ConsoleReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ladder.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ConsoleReader {
private static final BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

private ConsoleReader() {
}

public static String readLine() {
try {
return br.readLine();
} catch (IOException e) {
throw new IllegalArgumentException("입력 받는 중 예외가 발생했습니다.");
}
}
}
25 changes: 25 additions & 0 deletions src/main/java/ladder/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ladder.view;

import java.util.List;
import ladder.utils.ConsoleReader;

public class InputView {
private static final String INPUT_NAME_DESCRIPTION = "참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)";
private static final String INPUT_HEIGHT_DESCRIPTION = "최대 사다리 높이는 몇 개인가요?";
private static final String NAME_DELIMITER = ",";

private InputView() {
}

public static List<String> inputPlayerNames() {
System.out.println(INPUT_NAME_DESCRIPTION);
String rawNames = ConsoleReader.readLine();

return List.of(rawNames.split(NAME_DELIMITER));
}

public static int inputLadderHeight() {
System.out.println(INPUT_HEIGHT_DESCRIPTION);
return Integer.parseInt(ConsoleReader.readLine());
}
}
Loading