[테스트 주도 개발] 13장_진짜로 만들기

모든 중복을 제거하기 전에는 “5달러 + 5달러” 테스트는 끝난 것이 아니다. 코드 중복은 없지만, 가짜 구현에 있는 10 달러는 사실 테스트 코드에 있는 “5달러 + 5달러” 와 같다.

1
2
3
Money reduce(Expression source, String to) {
return Money.dollar(10);
}
1
2
3
4
5
6
7
8
@Test
void testSimpleAddition() {
Money five = Money.dollar(5);
Expression sum = five.plus(five); // here
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}

이전에는, 가짜 구현이 있을 때 진짜 구현으로 작업해가는 것이 명확했다. 이번에는 어떻게 거꾸로 작업해야할지 명확하지 않다. 그래서 조금 불확실하지만 순방향으로 작업해보자.
우선, Money.plus() 는 그냥 Money 가 아닌, Expression(Sum) 을 반환해야한다. 두 Money 의 합은 Sum 이어야한다.

1
2
3
4
5
6
7
8
@Test
void testPlusReturnsSum(){
Money five = Money.dollar(5);
Expression result = five.plus(five);
Sum sum = (Sum) result;
assertEquals(five, sum.augend);
assertEquals(five, sum.addend);
}

위 코드를 컴파일하기위해서는, augend, addend 필드를 가진 Sum 클래스가 필요하다.

1
2
3
4
class Sum {
Money augend;
Money addend;
}

다시 실행해보면, Money.plus() 는 Sum 이 아닌 Money 를 반환하게 되어 있어서, ClassCastExceptoin 을 발생시킨다. 그래서, 다음 처럼 수정하자.

1
2
3
4
// Money
Expression plus(Money addend){
return new Sum(this, addend)
}

Sum 생성자도 필요하다.

1
2
3
4
5
6
7
8
class Sum {
Money augend;
Money addend;

Sum(Money augend, Money addend) {

}
}

그리고 Sum 은 Expression 의 일종이어야한다.

1
2
3
4
5
6
7
8
class Sum implements Expression{
Money augend;
Money addend;

Sum(Money augend, Money addend) {

}
}

이제 컴파일 되는 상태이지만, 테스트는 여전히 실패한다. 왜냐하면, Sum 생성자에서 필드를 설정하지 않았기 때문이다.

1
2
3
4
5
6
7
8
9
class Sum implements Expression{
Money augend;
Money addend;

Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}
}

이제 Bank.reduce() 는 Sum 을 전달받는다.

1
2
3
4
5
6
7
@Test
void testReduceSum(){
Expression sum = new Sum(Money.dollar(3), Money.dollar(4));
Bank bank = new Bank();
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(7), result);
}

위 테스트는 테스트가 깨지도록 인자를 선택했다. Bank 클래스를 수정하자.

1
2
3
4
5
6
7
8
class Bank {

Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
int amount = sum.augend.amount + sum.addend.amount;
return new Money(amount, to);
}
}

위 코드는 다음 이유로 지저분하다.

  1. Casting. 위 코드는 모든 Expression 에 대해 작동해야한다.
  2. public 필드와 그 필드들에 대한 두 단계에 걸친 레퍼런스.

우선, 외부에서 접근 가능한 필드 몇개를 들어내기 위해 메서드 본문을 Sum 으로 옮길 수 있다.

1
2
3
4
5
6
7
class Bank {

Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
return sum.reduce(to);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Sum implements Expression {
Money augend;
Money addend;

Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}

Money reduce(String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}
}

다음 테스트를 작성하자.

1
2
3
4
5
6
@Test
void testReduceMoney() {
Bank bank = new Bank();
Money result = bank.reduce(Money.dollar(1), "USD");
assertEquals(Money.dollar(1), result);
}
1
2
3
4
5
6
7
8
class Bank {

Money reduce(Expression source, String to) {
if (source instanceof Money) return (Money) source;
Sum sum = (Sum) source;
return sum.reduce(to);
}
}

초록 막대이다. 리펙토링을 하자.
클래스를 명시적으로 검사하는 코드가 있을 때에는 항상 다형성을 사용하자. Sum 은 reduce(Stirng) 을 구현하므로, Money 도 그것을 구현하게 하면 reduce 를 Expression 인터페이스에 추가할 수 있다.

1
2
3
4
5
6
7
8
class Bank {

Money reduce(Expression source, String to) {
if (source instanceof Money) return (Money) source.reduce(to);
Sum sum = (Sum) source;
return sum.reduce(to);
}
}
1
2
3
4
//Money
public Money reduce(String to){
return this;
}

이제, Expression 인터페이스에 reduce(String) 을 추가하자.

1
2
3
interface Expression {
Money reduce(String to);
}

지저분한 캐스팅과 클래스 검사 코드를 제거할 수 있다.

1
2
3
4
5
6
class Bank {

Money reduce(Expression source, String to) {
return source.reduce(to);
}
}

Expression 과 Bank 에 이름이 동일하지만 매개 변수 형이 다른 메서드가 있다는 것이 만족스럽지 않다.


테스트 주도 개발 <켄트 벡>

Comments