책임 주도 설계
- 오브젝트 책의 책임 주도 설계 부분을 읽어보자
- 현장에서 책임 주도 설계를 바로 적용하려면 어렵다.
- 책임이란 객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 유지해야 하는 정보와 수행할 수 있는 행동에 대해 개략적으로 서술한 문장이다.
- 즉, 객체가 무엇을 알고 있는가와 무엇을 할 수 있는가로 책임이 구성된다.
- 객체가 할 수 있는 것
- 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
- 객체가 알고있는 것
- 사적인 정보에 관해 아는 것
- 관련된 객체에 대해 아는 것
책임 주도 설계란?
- 프로그래밍의 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법
역할이란?
- 객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라고 부른다.
책임 주도 설계 과정
- 시스템의 책임을 파악하라
- 책임을 더 작은 책임으로 분할한다
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우, 이를 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
결론
구현에 집중하는 게 쉽지만 책임 주도 설계는 자연스럽게 객체의 구현이 아닌 책임에 집중할 수 있게 한다. 구현이 아닌 책임에 집중하는 것이 중요한 이유는 유연하고 견고한 객체지향 시스템을 위해 가장 중요한 재료가 바로 책임이기 때문이다.
책임 주도 설계로 자동차 경주 게임을 구현해보자
- 시스템 책임
- 행위를 먼저 찾아보자.
- 자동차 경주 게임을 하려면 ‘경주를 하라’가 필요하다.
- 책임을 파악한 후 책임을 할당할 객체를 바로 결정하지 않아도 된다.
- ‘경주를 하라’ 라는 책임을 발견했다
- 더 작은 책임으로 분할하자
- 경주를 하라
- 이동 가능 여부를 결정하라
- 이동하라
- 경주를 하라
- 책임을 수행할 수 있는 적절한 객체를 할당하자
- Car 하나에 race(), canMove(), move() 메서드를 모두 구현해줄 수 있다.
- 그러나 좀 더 생각해보면 아래와 같은 구조를 생각할 수 있다.
- Car race(), RandomNo canMove(), Position move()
- 구현
- 인터페이스를 먼저 작성하고 의존 관계를 어떻게 가지는지 생각해봐야한다.
- 책임에 해당하는 부분만 일단 인터페이스로 만든다
public interface Racing {
void race(MoveStrategy moveStrategy);
}
public interface MoveStrategy {
boolean canMove();
}
public interface Movable {
void move();
}
public class PositionTest {
@Test
void 이동() {
Position position = new Position();
position.move();
assertThat(positon.getPosition()).isEqualTo(1);
}
}
- 여기서 현재 상태를 변경하는 메서드를 구현해야 하는데 최대한 immutable 하게 만들어보자
public class Position implements Movable {
// ...
@Override
public void move1() { // 보통 이런 식으로 짜는데 이렇게 하면 move() 라는 메서드에 상태를 변경하는 메서드가 있게 된다.
this.position++;
}
@Override
public Position move2() { // 직접 set하는 메서드가 없어 immutable Object가 되기 때문에 안전한 코드가 된다. 인스턴스가 많아져서 느려질 수 있지만 기술의 발전으로 커버가 된다. 이 방법을 사용하자.
return new Position(this.position + 1);
}
}
- 랜덤값을 테스트하기 위해 MoveStrategy를 구현하는 RandomMoveStrategy 클래스를 만든다
public class RandomMoveStrategy implements MoveStrategy {
private int no;
public RandomMoveStrategy(int no) {
this.no = no;
}
@Override
public boolean carMove() {
return no >= 4;
}
}
public class Car implements Racing {
private Movable movable = new Position(); // 일단 만들어 놓고 해보는데 Movable 인터페이스가 필요없어질 수 있다. 그냥 Position 클래스만 필요할 수도...
@Override
public void race(MoveStrategy moveStrategy) {
if(moveStrategy.canMove()) {
moveable.move();
}
}
}
race()
메서드 안에 들어가는MoveStrategy
인터페이스가 상당히 중요한 부분이다.MoveStrategy
인터페이스를 구현함으로써 조건이 있는RandomMoveStrategy
클래스를 받을 수 있고 전진(canMove()의 반환값이 항상 true
)만 하는ForwardMoveStrategy
클래스 또한 받을 수 있게 된다.ForwardMoveStrategy
클래스는 정적 팩토리 메서드로getInstance()
를 통해 매개변수 없이 받을 수 있다.
public class ForwardMoveStrategy implements MoveStrategy {
private static ForwardMoveStrategy forwardMoveStrategy = new ForwardMoveStrategy();
private ForwardMoveStrategy() {
}
public static ForwardMoveStrategy getInstance() {
return forwardMoveStrategy;
}
@Override
public boolean isMovable() {
return true;
}
}
- 이와 같이 구현하면 실제로 실행할 때는
RandomMoveStrategy
클래스를 통해서 랜덤으로 받지만 테스트해줄 때 전진을 원하면ForwardMoveStrategy
를 써주면 되기 때문에 테스트하기 훨씬 쉬워진다. - 그러나 쉬운 작업은 아니다. 처음부터 인터페이스간의 설계로 생각하기가 어렵다.
class Test {
@Test
void test() {
MoveStrategy randomMoveStrategy = new RandomMoveStrategy(number);
MoveStrategy forwardMoveStrategy = ForwardMoveStrategy.getInstance();
assertThat(randomMoveStrategy.canMove()).isEqualTo(expected);
assertThat(forwardMoveStrategy.canMove()).isEqualTo(true);
}
}
결론
- 시스템의 책임을 파악하자.
- 더 작은 책임으로 분리해 구현하는데 책임을 위주로 인터페이스부터 만들어보자.
- 일단 쓰레기 같은 코드를 만들어서 최대한 빠르게 기능 구현을 하자
- 지속적인 리펙토링과 TDD 사이클을 반복해 설계의 품질을 높혀 나가자
- 가능하면 인스턴스가 많이 만들어져도 immutable하게 만들자