Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ out/

### VS Code ###
.vscode/
.DS_STORE
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public class Line {
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- 배열 대신 컬렉션을 사용한다.
- Java Enum을 적용한다.
- 모든 원시 값과 문자열을 포장한다
- 모든 원시 값과 문자열을 포장한다.
- 줄여 쓰지 않는다(축약 금지).
- 일급 컬렉션을 쓴다.

Expand Down
15 changes: 15 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
1. 참여할 사람 이름 입력 -> 조건 : 사람 최소 2명, 이름은 쉼표(,)로 구분, 이름 최대 5글자, 이름 빈칸 X
2. 사다리 게임 높이 입력 -> 조건 : 최소 높이 2이상, 높이 숫자만 입력
3. 사다리 게임 -> 조건 : 사람
4. 실행 결과: 사람 이름, 사다리 동시 출력, 사람 이름 5글자 맞춰서 출력, 라인 겹치지 않게 출력

- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
- 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- 배열 대신 컬렉션을 사용한다.
- Java Enum을 적용한다.
- 모든 원시 값과 문자열을 포장한다.
- 줄여 쓰지 않는다(축약 금지).
- 일급 컬렉션을 쓴다.
12 changes: 12 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import controller.GameController;
import view.InputView;
import view.OutPutView;

public class Application {
public static void main(String[] args) {
InputView inputView = new InputView();
OutPutView outPutView = new OutPutView();
GameController gameController = new GameController(inputView,outPutView);
gameController.playGame();
}
}
37 changes: 37 additions & 0 deletions src/main/java/controller/GameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package controller;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;
import java.util.List;
import model.Ladder;
import model.User;
import model.Users;
import view.InputView;
import view.OutPutView;

public class GameController {

private final InputView inputView;
private final OutPutView outPutView;

public GameController(InputView inputView, OutPutView outPutView) {
this.inputView = inputView;
this.outPutView = outPutView;
}

public void playGame() {
Users users = createUsers();
int ladderHeight = inputView.createLadderHeight();
Ladder ladder = new Ladder(users.getUserCount(), ladderHeight);
outPutView.gameResult(users, ladder);

}

private Users createUsers() {
List<String> userNames = inputView.createUser();

return userNames.stream()
.map(i -> new User(i))
.collect(collectingAndThen(toList(), i -> new Users(i)));
Comment on lines +34 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

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

필수는 아니지만, 메서드 참조식의 사용도 고려해볼 만 합니다.
User::new로도 '스트림의 원소를 User 생성자의 유일한 파라미터로 넣어 생성자를 호출함'을 알 수 있으니까요!

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

public enum Direction {
RIGHT, NEUTRAL, LEFT
}
29 changes: 29 additions & 0 deletions src/main/java/model/Ladder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package model;

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

public class Ladder {
private static final int MIN_LADDER_HEIGHT = 2;
private final List<Line> lines;

public Ladder(int userCount, int height) {
validateHeight(height);

this.lines = new ArrayList<>();

for (int i = 0; i < height; i++) {
lines.add(new Line(userCount));
}
}

private void validateHeight(int height) {
if (height < MIN_LADDER_HEIGHT) {
throw new IllegalArgumentException("사다리 높이는 최소 2이상이어야 합니다.");
}
}

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

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

import java.util.Random;

public class Line {

private final Random random = new Random();

private final List<Direction> points;

public Line(int personCount) {

points = new ArrayList<>();
createLine(personCount,points);
Comment on lines +16 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

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

코드를 읽으면서 이 부분이 좀 의아했습니다.
아래의 createLint()에서 points 필드에 값을 할당하는 로직이 이미 존재합니다.
points를 빈 리스트로 초기화할 필요가 없어 보이네요!

또한 createLine()은 메서드 내부에서 값을 변경하고 있습니다.
이 메서드 내부에서 무슨 일이 일어났는지 구현부를 모두 들여다보지 않아도 되게끔
createLine()이 반환값을 가지게 하면 훨씬 읽기 좋을 것 같네요. 예를 들면

points = new ArrayList<>();
createLine(personCount, points);

이거보다

points = createLine(personCount);

이게 훨씬 이해하기 쉬워 보이네요.
createLine을 눌러서 무슨 일이 일어나는지 확인할 필요가 없기 때문이죠!

}

private void createLine(int personCount, List<Direction> points){
Direction previousDirection = Direction.NEUTRAL;

for (int i = 0; i < personCount - 1; i++) {
Direction newDirection = createDirection(previousDirection);
points.add(newDirection);
previousDirection = updatePreviousDirection(previousDirection, newDirection);
}
// 마지막 포인트는 항상 NEUTRAL
points.add(Direction.NEUTRAL);
}

private Direction createDirection(Direction previousDirection) {
if (previousDirection == Direction.LEFT) {
return Direction.NEUTRAL;
}
if (random.nextBoolean()) {
return Direction.RIGHT;
}
return Direction.NEUTRAL;
}

private Direction updatePreviousDirection(Direction previousDirection, Direction newDirection) {
// 오른쪽으로 가는 경우 다음 포인트는 왼쪽으로 갈 수 없습니다.
if (newDirection == Direction.RIGHT) {
return previousDirection = Direction.LEFT;
}
return previousDirection = Direction.NEUTRAL;
Comment on lines +42 to +47
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기서 첫째 파라미터인 previousDirection은 없어도 될 것 같아요!
return Direcetion.LEFT를 해도 똑같으니까요. 이 부분도 흐름을 이해하기 어려웠던 이유 중에 하나가 되었던 것 같아요!

}
Comment on lines +20 to +48
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. LEFT, NEUTRAL, RIGHT가 존재한다는 점,
  2. prev와 new를 사용해서 연속된 라인이 생기지 않게 검사하는 점,
  3. Direction 리스트의 변수명이 Point로 되어있는 점 등

천천히 읽으면서 분석해야 알 수 있는 부분이 많아서
제가 이 클래스를 처음 읽었을 때 이해하는 데 시간이 오래 걸렸었습니다.

다른 구현 예시를 보면서 좀 더 단순하게 만들 수 있을지 고민해보거나,
시간or아이디어가 충분하지 않을 경우에는 전체적인 흐름을 설명하는 주석을 달아주는 것도 좋을 것 같아요!


public List<Direction> getPoints() {
return this.points;
}
}
34 changes: 34 additions & 0 deletions src/main/java/model/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package model;

public class User {
private static final int MAX_NAME_LENGTH = 5;
private static final String BLANK = " ";

private final String userName;

public User(String userName) {
validateUserName(userName);
this.userName = userName;
}

private void validateUserName(String name) {
validateNameLength(name);
validateBlankInName(name);
}

private void validateNameLength(String name){
if(name.isEmpty() || name.length() > MAX_NAME_LENGTH){
throw new IllegalArgumentException("유저 이름은 1~5자리 글자여야 합니다.");
}
}

private void validateBlankInName(String name){
if(name.contains(BLANK)){
throw new IllegalArgumentException("유저 이름은 빈칸이 들어갈 수 없습니다.");
}
}

public String getUserName(){
return this.userName;
}
}
32 changes: 32 additions & 0 deletions src/main/java/model/Users.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package model;

import java.util.List;

public class Users {
private static final int MIN_USERS = 2;

private final List<User> users;

public Users(final List<User> users) {
validateUsersSizeValidate(users);
this.users = users;
}

private void validateUsersSizeValidate(List<User> users) {
validateUsers(users);
}

private void validateUsers(List<User> users) {
if (users.size() < MIN_USERS) {
throw new IllegalArgumentException("유저는 2명 이상이어야 합니다.");
}
}

public int getUserCount() {
return users.size();
}

public List<String> getUserName() {
return users.stream().map(i -> i.getUserName()).toList();
}
}
40 changes: 40 additions & 0 deletions src/main/java/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package view;

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class InputView {

private static final String separation = ",";

private Scanner sc = new Scanner(System.in);

public List<String> createUser(){
System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)");
String name = sc.nextLine();
validateCreateUser(name);

return Arrays.stream(name.split(separation)).toList();
}

public int createLadderHeight(){
System.out.println("최대 사다리 높이는 몇 개인가요?");
int ladderHeight = sc.nextInt();
validateCreateHeight(ladderHeight);

return ladderHeight;
}

private void validateCreateUser(String name){
if(name.isEmpty()||name.endsWith(separation)){
throw new IllegalArgumentException("잘못된 입력입니다.");
}
}

private void validateCreateHeight(int ladderHeight){
if(ladderHeight<2){
throw new IllegalArgumentException("2이상의 숫자만 입력하세요");
}
}
}
63 changes: 63 additions & 0 deletions src/main/java/view/OutPutView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package view;

import java.util.List;

import model.Direction;
import model.Ladder;
import model.Line;
import model.Users;

public class OutPutView {

private static final int MAX_NAME = 5;
private static final String BLANK = " ";
private static final String POINT = "-----";
private static final String LINE = "|";

public void gameResult(Users users, Ladder ladder) {
System.out.println("실행결과");
printUserNames(users);
printLadder(ladder);
}

private void printUserNames(Users users) {
StringBuilder sb = new StringBuilder();
for (String name : users.getUserName()) {
remakeNameFormat(sb, name);
}
System.out.println(sb);
}

private void remakeNameFormat(StringBuilder sb, String name) {
int blankNum = MAX_NAME - name.length();
if (name.length() < MAX_NAME) {
sb.append(BLANK.repeat(blankNum));
}
sb.append(name).append(BLANK);
}

private void printLadder(Ladder ladder) {
List<Line> lineList = ladder.getLines();
StringBuilder sb = new StringBuilder();
for (Line line : lineList) {
printLadderPoint(sb, line.getPoints());
}
System.out.println(sb);
}

private void printLadderPoint(StringBuilder sb, List<Direction> directions) {
sb.append(BLANK.repeat(MAX_NAME));
for (int i = 0; i < directions.size() - 1; i++) {
sb.append(LINE)
.append(printDirections(directions.get(i)));
}
sb.append(LINE).append(System.lineSeparator());
}

private String printDirections(Direction direction) {
if (direction == Direction.RIGHT) {
return POINT;
}
return BLANK.repeat(MAX_NAME);
}
}
35 changes: 35 additions & 0 deletions src/test/java/ladder/LadderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ladder;

import model.Ladder;
import model.Line;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class LadderTest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

테스트 클래스는 public이 아니어도 괜찮습니다.
하나의 테스트는 그 자체만으로 독립적인 기능을 수행해야 하고,
이는 LineTest를 작성하다가 new LadderTest() 같은 걸 호출할 일은 없다는 것을 의미하니까요!
SonarLint 툴에서도 public keyword를 자제하라고 말하고 있네요. 툴 내부에서 그 이유 또한 설명하니 읽어보세요!

@DisplayName("주어진 높이에 맞게 사다리 생성")
@Test
void newLadderTest() {
int userCount = 5;
int height = 6;

Ladder ladder = new Ladder(userCount, height);
List<Line> lines = ladder.getLines();
assertEquals(height, lines.size());
}

@DisplayName("사다리의 높이가 2개 미만일 경우 예외 발생")
@Test
void ValidateLadderHeightTest() {
int userCount = 5;
int height = 1;

assertThatThrownBy(() -> new Ladder(userCount, height))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("사다리 높이는 최소 2이상이어야 합니다.");
}
}
Loading