diff --git a/README.md b/README.md index cc0f757d0b..d9e273e68d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ > - 익명 클래스를 람다로 전환 : nextstep.fp.CarTest 이동, 정지 method - [x] 람다 실습 2 > - 람다를 활용해 중복 제거 : nextstep.fp.Lambda의 sumAll, sumAllEven, sumAllOverThree method -- [x] 흰트 +- [x] 힌트 > - 변경되는 부분과 변경되지 않는 부분의 코드를 분리한다. > - 변경되는 부분을 인터페이스로 추출한다. > - 인터페이스에 대한 구현체를 익명 클래스(anonymous class)로 구현해 메소드의 인자로 전달한다. @@ -76,3 +76,71 @@ > - Arrays.stream()을 이용해 배열을 stream으로 생성할 수 있다. > - 일치하는 값 하나를 추출할 때 findFirst() 메소드를 활용 가능하다. > - Optional의 orElseThrow() 메소드 활용해 구현한다. + +### 코멘트 +- [x] 함수형 인터페이스는 애너테이션 `@FunctionalInterface`으로 선언 + +--- +## STEP2. 사다리(생성) +### 기능 요구사항 +> - 사다리 게임에 참여하는 사람에 이름을 최대5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다. +> - 사람 이름은 쉼표(,)를 기준으로 구분한다. +> - 사람 이름을 5자 기준으로 출력하기 때문에 사다리 폭도 넓어져야 한다. +> - 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다. + > - |-----|-----| 모양과 같이 가로 라인이 겹치는 경우 어느 방향으로 이동할지 결정할 수 없다. + +- [x] 사다리 게임에 참여하는 사람의 이름은 최대 5글자이다. +- [x] 사다리 게임에 참여하는 사람의 이름은 쉼표(,)를 기준으로 구분한다. +- [x] 사다리 게임은 참여하는 사람의 이름과 최대 사다리 높이를 전달받는다. +- [x] 사다리 타기는 (참여하는 사람의 수 - 1) 만큼 라인이 생성된다. +- [x] 사다리 타기는 라인이 겹치지 않는다. +- [x] 사다리는 랜덤값 등 사다리 생성 전략 따라서 생성한다. +- [x] 사다리의 폭은 최대 글자 수 만큼으로 출력한다. + +### 프로그래밍 요구사항 +> - 자바 8의 스트림과 람다를 적용해 프로그래밍한다. +> - 규칙 6: 모든 엔티티를 작게 유지한다. + +실행 결과 +``` +참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요) +pobi,honux,crong,jk + +최대 사다리 높이는 몇 개인가요? +5 + +실행결과 + +pobi honux crong jk + |-----| |-----| + | |-----| | + |-----| | | + | |-----| | + |-----| |-----| +``` + +### 힌트 +> - 2차원 배열을 ArrayList, Generic을 적용해 구현하면 ArrayList>와 같이 이해하기 어려운 코드가 추가된다. +> - 사다리 게임에서 한 라인의 좌표 값을 가지는 객체를 추가해 구현해 본다. +> - 아래와 같이 Line 객체를 추가하면 ArrayList> 코드를 ArrayList과 같이 구현하는 것이 가능해 진다. + +```java +public class Line { + private List points = new ArrayList<>(); + + public Line (int countOfPerson) { + // 라인의 좌표 값에 선이 있는지 유무를 판단하는 로직 추가 + } + + [...] +} +``` + +### 코멘트 +- [x] 사소하지만 컨벤션을 준수하면 좋겠네요 +- [x] InputView > `inputNames()` 입력 책임에 맞게 이름을 리스트로 제공해 주면 어떨까요? 그러면 비즈니스 로직에서 파싱할 필요가 없어져서 로직에만 집중할 수 있습니다. +- [x] OutputView > `printLadders()` depth가 깊어보이는데 스트림과 메소드 추출을 활용하면 좋겠습니다! +- [x] LineTest > `getLine()` 대신 `new Line()`으로 비교해 볼 수 있게 만들어봐도 좋겠네요 +- [ ] Lines > `validLadderLength()` 양수 값 객체를 만드는 방법도 있겠네요! `규칙 6: 모든 엔티티를 작게 유지한다.` +- [ ] LadderFactory > 상태를 가지는 대신 Factory가 객체를 생성해주는 방향으로 만들어보면 어떨까요? `var ladder = factory.line(names, hieght)` +- [x] Line > `createLine()` boolean 연산 값이 참일 때 라인 요소를 false로 세팅하네요. 가독성에 신경써서 이게 어떤 작업인지 메소드 이름을 통해 표현해 보면 좋겠습니다! \ No newline at end of file diff --git a/src/main/java/ladder/LadderApplication.java b/src/main/java/ladder/LadderApplication.java new file mode 100644 index 0000000000..9ed55512f2 --- /dev/null +++ b/src/main/java/ladder/LadderApplication.java @@ -0,0 +1,18 @@ +package ladder; + +import ladder.domain.LadderFactory; +import ladder.view.InputView; +import ladder.view.OutputView; + + +public class LadderApplication { + + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + + LadderFactory ladderFactory = new LadderFactory(inputView.inputNames(), inputView.inputHeight()); + outputView.printNames(ladderFactory.getNames()); + outputView.printLadder(ladderFactory.getLadder()); + } +} diff --git a/src/main/java/ladder/domain/LadderFactory.java b/src/main/java/ladder/domain/LadderFactory.java new file mode 100644 index 0000000000..6847be0f22 --- /dev/null +++ b/src/main/java/ladder/domain/LadderFactory.java @@ -0,0 +1,25 @@ +package ladder.domain; + +import java.util.List; + +public class LadderFactory { + private final Names names; + private final Lines lines; + + public LadderFactory(List names, int height) { + this(names, height, new RandomLineGenerator()); + } + + public LadderFactory(List names, int height, LineGenerator lineGenerator) { + this.names = new Names(names); + this.lines = new Lines(height, this.names.connectSize(), lineGenerator); + } + + public List> getLadder() { + return lines.getList(); + } + + public List getNames() { + return names.getAll(); + } +} diff --git a/src/main/java/ladder/domain/Line.java b/src/main/java/ladder/domain/Line.java new file mode 100644 index 0000000000..7f317bc4f4 --- /dev/null +++ b/src/main/java/ladder/domain/Line.java @@ -0,0 +1,65 @@ +package ladder.domain; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Line { + private final List line; + + public Line(int size, LineGenerator generator) { + this(createLine(size, generator)); + } + + public Line(boolean... line) { + this(createLine(line)); + } + + public Line(List line) { + this.line = line; + } + + private static List createLine(int size, LineGenerator generator) { + if (size < 1) { + throw new IllegalArgumentException("Line size should be greater than 0"); + } + return createLine(generate(size, generator)); + } + + private static List createLine(boolean[] line) { + return IntStream.range(0, line.length) + .mapToObj(i -> line[i]) + .collect(Collectors.toList()); + } + + private static boolean[] generate(int size, LineGenerator generator) { + boolean[] line = new boolean[size]; + line[0] = generator.isConnected(); + + for (int i = 1; i < size; i++) { + line[i] = (!line[i - 1] && generator.isConnected()); + } + + return line; + } + + public List getList() { + return Collections.unmodifiableList(line); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Line line1 = (Line) o; + return Objects.equals(line, line1.line); + } + + @Override + public int hashCode() { + return Objects.hashCode(line); + } + +} diff --git a/src/main/java/ladder/domain/LineGenerator.java b/src/main/java/ladder/domain/LineGenerator.java new file mode 100644 index 0000000000..1cec241fc6 --- /dev/null +++ b/src/main/java/ladder/domain/LineGenerator.java @@ -0,0 +1,5 @@ +package ladder.domain; + +public interface LineGenerator { + boolean isConnected(); +} diff --git a/src/main/java/ladder/domain/Lines.java b/src/main/java/ladder/domain/Lines.java new file mode 100644 index 0000000000..35966222d3 --- /dev/null +++ b/src/main/java/ladder/domain/Lines.java @@ -0,0 +1,32 @@ +package ladder.domain; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Lines { + private final List lines; + + public Lines(int height, int width, LineGenerator generator) { + validLadderLength(height); + validLadderLength(width); + this.lines = createLadder(height, width, generator); + } + + private void validLadderLength(int value) { + if (value < 1) + throw new IllegalArgumentException("The ladder height should be larger than zero."); + } + + private List createLadder(int height, int width, LineGenerator generator) { + return IntStream.range(0, height) + .mapToObj(i -> new Line(width, generator)) + .collect(Collectors.toList()); + } + + public List> getList() { + return lines.stream() + .map(Line::getList) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/src/main/java/ladder/domain/Names.java b/src/main/java/ladder/domain/Names.java new file mode 100644 index 0000000000..1fcc3719fa --- /dev/null +++ b/src/main/java/ladder/domain/Names.java @@ -0,0 +1,31 @@ +package ladder.domain; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class Names { + private final List names; + + public Names(String... names) { + this(Arrays.asList(names)); + } + + public Names(List names) { + this.names = names; + validateNameMaxLength(names); + } + + private void validateNameMaxLength(List names) { + if (names.stream().anyMatch(name -> name.length() > 5)) + throw new IllegalArgumentException("The name should be up to 5 letters."); + } + + public List getAll() { + return Collections.unmodifiableList(names); + } + + public int connectSize() { + return names.size() - 1; + } +} diff --git a/src/main/java/ladder/domain/RandomLineGenerator.java b/src/main/java/ladder/domain/RandomLineGenerator.java new file mode 100644 index 0000000000..3f66e4644e --- /dev/null +++ b/src/main/java/ladder/domain/RandomLineGenerator.java @@ -0,0 +1,12 @@ +package ladder.domain; + +import java.util.Random; + +public class RandomLineGenerator implements LineGenerator { + public static final Random random = new Random(); + + @Override + public boolean isConnected() { + return random.nextBoolean(); + } +} diff --git a/src/main/java/ladder/view/InputView.java b/src/main/java/ladder/view/InputView.java new file mode 100644 index 0000000000..39daa87b3c --- /dev/null +++ b/src/main/java/ladder/view/InputView.java @@ -0,0 +1,23 @@ +package ladder.view; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView { + private static final Scanner SCANNER = new Scanner(System.in); + + public List inputNames() { + System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"); + String names = SCANNER.nextLine(); + System.out.println(); + return Arrays.asList(names.split(",")); + } + + public int inputHeight() { + System.out.println("최대 사다리 높이는 몇 개인가요?"); + int height = SCANNER.nextInt(); + SCANNER.nextLine(); + return height; + } +} diff --git a/src/main/java/ladder/view/OutputView.java b/src/main/java/ladder/view/OutputView.java new file mode 100644 index 0000000000..7f764bd449 --- /dev/null +++ b/src/main/java/ladder/view/OutputView.java @@ -0,0 +1,33 @@ +package ladder.view; + +import java.util.List; +import java.util.stream.Collectors; + +public class OutputView { + public void printNames(List names) { + System.out.println("실행결과"); + System.out.println(); + + names.stream() + .map(name -> String.format("%5s ", name)) + .forEach(System.out::print); + + System.out.println(); + } + + public void printLadder(List> ladders) { + ladders.stream() + .map(this::convertLine) + .forEach(System.out::println); + } + + private String convertLine(List line) { + return line.stream() + .map(this::convertLineElement) + .collect(Collectors.joining("|", " |", "|")); + } + + private String convertLineElement(Boolean connected) { + return Boolean.TRUE.equals(connected) ? "-----" : " "; + } +} diff --git a/src/main/java/nextstep/fp/Lambda.java b/src/main/java/nextstep/fp/Lambda.java index 094ca80bbd..553cc38d41 100644 --- a/src/main/java/nextstep/fp/Lambda.java +++ b/src/main/java/nextstep/fp/Lambda.java @@ -45,6 +45,7 @@ private static int sumBy(List numbers, SumCondition condition) { .orElse(0); } + @FunctionalInterface public interface SumCondition { boolean match(int number); } diff --git a/src/main/java/nextstep/fp/StreamStudy.java b/src/main/java/nextstep/fp/StreamStudy.java index b6f5146576..065dbd92d2 100644 --- a/src/main/java/nextstep/fp/StreamStudy.java +++ b/src/main/java/nextstep/fp/StreamStudy.java @@ -4,7 +4,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; +import java.util.Arrays; +import java.util.List; import java.util.stream.Collectors; public class StreamStudy { diff --git a/src/main/java/nextstep/optional/User.java b/src/main/java/nextstep/optional/User.java index ffad10c060..c586aec6b5 100644 --- a/src/main/java/nextstep/optional/User.java +++ b/src/main/java/nextstep/optional/User.java @@ -11,18 +11,6 @@ public User(String name, Integer age) { this.age = age; } - public String getName() { - return name; - } - - public Integer getAge() { - return age; - } - - public boolean matchName(String name) { - return this.name.equals(name); - } - public static boolean ageIsInRange1(User user) { boolean isInRange = false; @@ -41,6 +29,18 @@ public static boolean ageIsInRange2(User user) { .isPresent(); } + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } + + public boolean matchName(String name) { + return this.name.equals(name); + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/nextstep/optional/Users.java b/src/main/java/nextstep/optional/Users.java index f51612fe73..ec2daa1e14 100644 --- a/src/main/java/nextstep/optional/Users.java +++ b/src/main/java/nextstep/optional/Users.java @@ -2,7 +2,6 @@ import java.util.Arrays; import java.util.List; -import java.util.Optional; public class Users { static final User DEFAULT_USER = new User("codesquad", 100); diff --git a/src/test/java/ladder/domain/LineTest.java b/src/test/java/ladder/domain/LineTest.java new file mode 100644 index 0000000000..da27989ac2 --- /dev/null +++ b/src/test/java/ladder/domain/LineTest.java @@ -0,0 +1,25 @@ +package ladder.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class LineTest { + + @Test + @DisplayName("사다리 라인을 생성한다.") + void createLine() { + assertThat(new Line(3, () -> true)).isEqualTo(new Line(true, false, true)); + assertThat(new Line(3, () -> false)).isEqualTo(new Line(false, false, false)); + } + + @Test + @DisplayName("사다리의 라인 하나가 반환된다.") + void getList() { + assertThat(new Line(true, false, true).getList()).isEqualTo(List.of(true, false, true)); + } + +} \ No newline at end of file diff --git a/src/test/java/ladder/domain/LinesFactoryTest.java b/src/test/java/ladder/domain/LinesFactoryTest.java new file mode 100644 index 0000000000..b18250ae10 --- /dev/null +++ b/src/test/java/ladder/domain/LinesFactoryTest.java @@ -0,0 +1,42 @@ +package ladder.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LinesFactoryTest { + + + @Test + @DisplayName("사다리 게임에 입력하는 사다리 높이는 양수여야 한다.") + void validateLadderHeightShouldBeLargerThan0() { + assertThatThrownBy(() -> new LadderFactory(List.of("sunny", "univ"), -1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("사다리 타기는 (참여하는 사람의 수 - 1) 만큼 라인이 생성된다.") + void getLadder() { + assertThat(new LadderFactory(List.of("red", "blue"), 2, () -> true).getLadder()) + .hasSize(2) + .allMatch(width -> width.size() == 1); + } + + @Test + @DisplayName("사다리 타기는 라인이 겹치지 않는다.") + void lineShouldNotOverlap() { + assertThat(new LadderFactory(List.of("red", "blue", "green"), 3, () -> true).getLadder()) + .allMatch(width -> !width.equals(List.of(true, true))); + } + + @Test + @DisplayName("사다리 타기 참여자의 이름을 얻는다.") + void getNames() { + assertThat(new LadderFactory(List.of("red", "blue", "green"), 1).getNames()).contains("red", "blue", "green"); + } +} + diff --git a/src/test/java/ladder/domain/LinesTest.java b/src/test/java/ladder/domain/LinesTest.java new file mode 100644 index 0000000000..b2f5d88fd8 --- /dev/null +++ b/src/test/java/ladder/domain/LinesTest.java @@ -0,0 +1,25 @@ +package ladder.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LinesTest { + + @Test + @DisplayName("사다리 높이는 양수여야 한다.") + void ladderHeightShouldBePositive() { + assertThatThrownBy(() -> new Lines(0, 2, () -> true)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new Lines(2, 0, () -> true)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("사다리 높이와 폭은 입력한대로 생성된다.") + void getList() { + assertThat(new Lines(3, 5, () -> true).getList()) + .hasSize(3) + .allMatch(line -> line.size() == 5); + } +} \ No newline at end of file diff --git a/src/test/java/ladder/domain/NamesTest.java b/src/test/java/ladder/domain/NamesTest.java new file mode 100644 index 0000000000..8949e3f85e --- /dev/null +++ b/src/test/java/ladder/domain/NamesTest.java @@ -0,0 +1,30 @@ +package ladder.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class NamesTest { + + @Test + @DisplayName("참여자 이름이 최대 글자수를 초과하면 에러가 발생한다.") + void exceedNameLengthLimit() { + assertThatThrownBy(() -> new Names("sunny", "universe")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("참여자 이름이 들어간 리스트가 반환된다.") + void getAllNames() { + assertThat(new Names("red", "blue", "green").getAll()).contains("red", "blue", "green"); + } + + @Test + @DisplayName("참여자 사이의 연결 개수를 반환한다.") + void getConnectSize() { + assertThat(new Names("red", "blue", "green").connectSize()).isEqualTo(2); + } + +} \ No newline at end of file diff --git a/src/test/java/nextstep/fp/StreamStudyTest.java b/src/test/java/nextstep/fp/StreamStudyTest.java index 22478c9f9b..ab73f18445 100644 --- a/src/test/java/nextstep/fp/StreamStudyTest.java +++ b/src/test/java/nextstep/fp/StreamStudyTest.java @@ -7,7 +7,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Arrays; -import java.util.Comparator; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +44,7 @@ public void printLongestWordTop100_ShouldOrderByLengthDesc() throws Exception { StreamStudy.printLongestWordTop100(); assertThat(out.toString().split(",")) .hasSize(100) - .isSortedAccordingTo((word1, word2) -> word2.length() - word1.length() ); + .isSortedAccordingTo((word1, word2) -> word2.length() - word1.length()); } @Test diff --git a/src/test/java/nextstep/optional/UserTest.java b/src/test/java/nextstep/optional/UserTest.java index b1ec7c8142..bfc6af4941 100644 --- a/src/test/java/nextstep/optional/UserTest.java +++ b/src/test/java/nextstep/optional/UserTest.java @@ -1,6 +1,5 @@ package nextstep.optional; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static nextstep.optional.User.ageIsInRange1;