Out -> In 접근근 방식 vs In -> Out 접근 방식
out -> in 접근 방식은 도메인 지식이 없거나 요구사항 복잡도가 높은 경우 적합
in -> out 접근 방식은 도메인 지식이 있거나 요구사항이 단순한 경우 적합
out -> in 접근 방식은 tdd로 하기가 쉽지 않다.
tdd로 수월하게 하기 위해서는 in -> out 방식으로 진행해야 한다.
Ladder: 전체 사다리게임의 이동
Line : 한개의 라인에 대해서 왼쪽, 오른쪽으로 이동할 지 결정
Ladder와 Line 중 어디서부터 구현을 시작하는 것이 좋을까?
위 다이어그램에서 Out의 시작점은 어느 곳을 의미할까? (Ladder)
Line 부터 완성한 다음 -> Ladder 구현 (in -> out 방식)
Ladder 부터 완성한 다음 -> Line 구현 (out -> in 방식)
Out부터 In으로 구현
- Ladder를 생성하는 부분과 생성한 Ladder를 실행(사다리 타기) 하는 부분을 분리
- TDD 구현시 특정 상태를 분리해 시작하는 것이 중요
- Ladder를 생성하는 부분은 무시하고, Ladder를 실행(설계에서 move) 하는 부분에서 시작
- 테스트하기 어려움. 바로 포기
- 기존 코드를 삭제하고 Line move()에서 다시 시작
- 도메인 지식을 쌓은 후 더 작은 단위로 분리할 객체는 없을까?
가장 작은 단위의 객체를 찾는 것이 중요하다.
가장 작은 단위의 객체로 point를 구현해보자
좌우에 사다리가 없어 아래쪽으로 내려가는 경우에 대한 구현이다.
public class PointTest {
@Test
void pass() {
Point point = new Point(false, false);
assertThat(point.move()).isEqualTo(Direction.SOUTH);
}
}
public enum Direction {
SOUTH;
}
첫번 째 test를 통과할 정도만 구현한다.
public class Point {
public Point(boolean left, boolean right) {
}
public Direction move() {
return Direction.SOUTH;
}
}
좌로 가는 경우, 우로 가는 경우도 test case를 추가해주자.
public class PointTest {
@Test
void left() {
Point point = new Point(true, false);
assertThat(point.move()).isEqualTo(Direction.LEFT);
}
@Test
void right() {
Point point = new Point(false, true);
assertThat(point.move()).isEqualTo(Direction.RIGHT);
}
@Test
void pass() {
Point point = new Point(false, false);
assertThat(point.move()).isEqualTo(Direction.SOUTH);
}
}
package ladder;
public enum Direction {
SOUTH, RIGHT, LEFT;
}
package ladder;
public class Point {
private final boolean left;
private final boolean current;
public Point(boolean left, boolean current) {
this.left = left;
this.current = current;
}
public Direction move() {
if (current) {
return Direction.RIGHT;
}
if (left) {
return Direction.LEFT;
}
return Direction.SOUTH;
}
}
예외 사항을 확인해본다.
public class PointTest {
@Test
void invalid() {
assertThatThrownBy(() -> {
new Point(true, true);
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
void left() {
Point point = new Point(true, false);
assertThat(point.move()).isEqualTo(Direction.LEFT);
}
@Test
void right() {
Point point = new Point(false, true);
assertThat(point.move()).isEqualTo(Direction.RIGHT);
}
@Test
void pass() {
Point point = new Point(false, false);
assertThat(point.move()).isEqualTo(Direction.SOUTH);
}
}
예외 사항을 구현한다.
package ladder;
public class Point {
private final boolean left;
private final boolean current;
public Point(boolean left, boolean current) {
if (left && current) {
throw new IllegalArgumentException("상태 값이 유효하지 않습니다.");
}
this.left = left;
this.current = current;
}
public Direction move() {
if (current) {
return Direction.RIGHT;
}
if (left) {
return Direction.LEFT;
}
return Direction.SOUTH;
}
}
Point 클래스를 다른 사용자가 사용하기에 좀 더 안전하게 코드를 구현하려면 어떻게 해야할까?
첫번째 값의 left는 false 이어야 한다는 것.
test 코드를 작성해보자.
public class PointTest {
@Test
void first() {
Point point = Point.first(true);
assertThat(point.move()).isEqualTo(Direction.RIGHT);
}
}
위 메서드를 다시 Point 클래스에서 구현해보자.
package ladder;
public class Point {
private final boolean left;
private final boolean current;
public Point(boolean left, boolean current) {
if (left && current) {
throw new IllegalArgumentException("상태 값이 유효하지 않습니다.");
}
this.left = left;
this.current = current;
}
public static Point first(boolean current) {
return new Point(false, current);
}
public Direction move() {
if (current) {
return Direction.RIGHT;
}
if (left) {
return Direction.LEFT;
}
return Direction.SOUTH;
}
}
이번에는 이전 값의 current 값을, 현재 값의 left로 사용하는 test case를 만들어보자.
public class PointTest {
@Test
void first() {
Point point = Point.first(true);
assertThat(point.move()).isEqualTo(Direction.RIGHT);
}
@Test
void next() {
Point point = Point.first(true).next(false);
assertThat(point.move()).isEqualTo(Direction.LEFT);
}
}
Point 클래스에서 메서드를 구현해보자.
package ladder;
public class Point {
private final boolean left;
private final boolean current;
public Point(boolean left, boolean current) {
if (left && current) {
throw new IllegalArgumentException("상태 값이 유효하지 않습니다.");
}
this.left = left;
this.current = current;
}
public static Point first(boolean current) {
return new Point(false, current);
}
public Point next(boolean current) {
return new Point(this.current, current);
}
public Direction move() {
if (current) {
return Direction.RIGHT;
}
if (left) {
return Direction.LEFT;
}
return Direction.SOUTH;
}
}
next 메서드에 대한 추가적인 test case를 작성해본다.
package ladder;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class PointTest {
@Test
void first() {
Point point = Point.first(true);
assertThat(point.move()).isEqualTo(Direction.RIGHT);
}
@Test
void next1() {
Point point = Point.first(true).next(false);
assertThat(point.move()).isEqualTo(Direction.LEFT);
}
@Test
void next2() {
Point point = Point.first(false).next(true);
assertThat(point.move()).isEqualTo(Direction.RIGHT);
}
@Test
void next3() {
Point point = Point.first(false).next(false);
assertThat(point.move()).isEqualTo(Direction.SOUTH);
}
@Test
void next4() {
assertThatThrownBy(() -> {
Point point = Point.first(true).next(true);
}).isInstanceOf(IllegalArgumentException.class);
}
}
last에 대한 test case를 만들어보자.
package ladder;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class PointTest {
@Test
void last() {
Point point = Point.first(true).next(false).last();
assertThat(point.move()).isEqualTo(Direction.SOUTH);
}
}
Point 클래스에서 last 메서드를 구현해보자.
package ladder;
public class Point {
private final boolean left;
private final boolean current;
public Point(boolean left, boolean current) {
if (left && current) {
throw new IllegalArgumentException("상태 값이 유효하지 않습니다.");
}
this.left = left;
this.current = current;
}
public static Point first(boolean current) {
return new Point(false, current);
}
public Direction move() {
if (current) {
return Direction.RIGHT;
}
if (left) {
return Direction.LEFT;
}
return Direction.SOUTH;
}
public Point next(boolean current) {
return new Point(this.current, current);
}
public Point last() {
return new Point(this.current, false);
}
}
test case를 만들어보자.
public class CrossTest {
@Test
void right() {
Point right = Point.first(true);
Cross cross = new Cross(1, right);
assertThat(cross.move()).isEqualTo(2);
}
}
test case가 통과하도록 Cross 클래스를 생성한 뒤, move 메서드를 구현하자
package ladder;
public class Cross {
private final int position;
private final Point point;
public Cross(int position, Point point) {
this.position = position;
this.point = point;
}
public int move() {
if (point.move() == Direction.RIGHT) {
return position + 1;
}
return 0;
}
}
left 에 해당하는 test case도 구현해주자
package ladder;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
public class CrossTest {
@Test
void right() {
Point right = Point.first(true);
Cross cross = new Cross(1, right);
assertThat(cross.move()).isEqualTo(2);
}
@Test
void left() {
Point left = Point.first(false);
Cross cross = new Cross(1, left);
assertThat(cross.move()).isEqualTo(0);
}
@Test
void south() {
Point south = Point.first(false);
Cross cross = new Cross(1, south);
assertThat(cross.move()).isEqualTo(1);
}
}
package ladder;
public class Cross {
private final int position;
private final Point point;
public Cross(int position, Point point) {
this.position = position;
this.point = point;
}
public int move() {
if (point.move() == Direction.RIGHT) {
return position + 1;
}
if (point.move() == Direction.LEFT) {
return position - 1;
}
return position;
}
}
Line 슈도 코드는 아래와 같이 나올 것이다.
package ladder;
import java.util.List;
public class Line {
private List<Cross> crosses;
public int move(int position) {
return crosses.get(position).move();
}
}
Ladder 슈도 코드는 아래와 같이 구현 될 것이다.
package ladder;
import java.util.List;
public class Ladder {
private List<Line> lines;
public int move(int position) {
int result = position;
for (Line line : lines) {
result = line.move(position);
}
return result;
}
}
책임 주도 설계로 사다리타기 재설계
인터페이스 우선적으로 구현한다면, 어떻게 인터페이스를 도출해 낼 수 있을 것인가를 먼저 고민하는 것도 상당히 좋은 프로그래밍 설계 연습이다.
처음부터 좋은 인터페이스를 도출하는 것은 잘 안된다.
그러나 도메인 지식이 쌓이고 프로그래밍 구현을 완료한 다음에, 이 부분을 인터페이스로 뽑으면 좋겠다라는 생각을 할 수 있다.
'강의 > TDD, Clean Code with Java 12기' 카테고리의 다른 글
로또 게임 - 강의 정리 (0) | 2021.09.07 |
---|---|
엘레강트 오브젝트 (0) | 2021.09.07 |
자동차 게임 프로젝트 - 강의 정리 (0) | 2021.09.03 |
옵셔널 (0) | 2021.08.25 |
스트림 실습 (0) | 2021.08.25 |