본문 바로가기

강의/TDD, Clean Code with Java 12기

사다리타기 교육

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