// Franc Class Franc times(int multiplier){ returnnew Franc(amount * multiplier); } // Dollar Class Dollar times(int multiplier){ returnnew Dollar(amount * multiplier); }
양쪽 모두 Money 를 반환하게 만들면 더 비슷해진다.
1 2 3 4 5 6 7 8 9
// Franc Class Money times(int multiplier){ returnnew Franc(amount * multiplier); } // Dollar Class Money times(int multiplier){ returnnew Dollar(amount * multiplier); }
이제, Money 의 두 하위 클래스는 많은 일을 하지 않으므로 제거하고 싶다. 한번에 이렇게 큰 단계를 밟는 것은 TDD 를 효과적으로 보여주기 적절치 않을 것 같다. 하위 클래스에 대한 직접적인 참조가 적어지면 하위 클래스를 제거하기 쉬울 것 같다.
1 2 3 4 5 6 7
@Test voidtestMultiplication(){ Dollar five = Money.dollar(5); assertEquals(new Dollar(10), five.times(2)); assertEquals(new Dollar(15), five.times(3)); }
구현 코드는 Dollar 를 생성해 반환한다.
1 2 3 4
// Money Class static Dollar dollar(int amount){ returnnew Dollar(amount); }
제목 : 영어에서 시간과 곱하기가 모드 ‘times’ 라는 점에서 착안한 말장난이다. 통화 개념을 어떻게 테스트하길 원하는가 ? 통화를 표현하기 위한 복잡한 객체를 만들 수도 있다. 그리고, 그 객체들이 필요한 만큼만 만들어지도록 하기 위해 flyweight factories 경량 팩토리를 사용할 수 있을 것이다. 하지만, 당분간 대신 문자열을 쓰자.
다중 통화 사용에 대한 내용을 시스템의 나머지 코드에 숨기고 싶다. ( 2달러 + 3CHF ) * 5 를 보자. Money 가 수식의 가장 작은 단위가 된다. 연산의 결과로 Expression 들이 생기는데, 그 중 하나가 sum 이다. 연산이 완료되면 환율을 이용해서 결과 expression 을 단일 통화로 축약할 수 있다. 이런식으로 테스트할 수 있다.
@Test voidtestMultiplication(){ Dollar five = new Dollar(5); Dollar product = five.times(2); assertEquals(10, product.amount); product = five.times(3); assertEquals(15, product.amount); }
첫 번째 assertion 을 Dollar 와 Dollar 를 비교하는 것으로 재작성할 수 있다.
1 2 3 4 5 6 7 8
@Test voidtestMultiplication(){ Dollar five = new Dollar(5); Dollar product = five.times(2); assertEquals(new Dollar(10), product); product = five.times(3); assertEquals(15, product.amount); }
두 번째 assertion 도 마찬가지다.
1 2 3 4 5 6 7 8
@Test voidtestMultiplication(){ Dollar five = new Dollar(5); Dollar product = five.times(2); assertEquals(new Dollar(10), product); product = five.times(3); assertEquals(new Dollar(15), product); }
이제 임시 변수인 product는 필요없다.
1 2 3 4 5 6
@Test voidtestMultiplication(){ Dollar five = new Dollar(5); assertEquals(new Dollar(10), five.times(2)); assertEquals(new Dollar(15), five.times(3)); }
테스트를 고치고 나니, Dollar 의 amount 인스턴스 변수를 사용하는 코드는 Dollar 자신 밖에 없다. 따라서, 변수를 private 으로 변경 가능하다.
1
privateint amount;
동치성 테스트가, 동치성에 대한 코드가 정확히 동작하는 것을 검증하는데 실패하면다면 곱하기 테스트 역시, 곱하기에 대한 코드가 정확히 동작한다는 것을 검증하는데 실파헤가 된다. 이것은 TDD 를 하면서 적극적으로 관리해야할 위험 요소이다.
우선 Dollar 객체와 비슷하지만 프랑을 표현할 수 있는 객체가 필요하다. Dollar 테스트를 복사한 후 수정해보자.
1 2 3 4 5 6 7
@Test voidtestFrancMultiplication(){ Franc five = new Franc(5); assertEquals(new Franc(10), five.times(2)); assertEquals(new Franc(15), five.times(3)); }
4장에서 테스트를 단순화해놓아서 지금하는 작업이 더 쉬워졌다. 테스트 주기에는 서로 다른 단계들이 있다는 것을 다시 정리하자.
테스트 작성
컴파일 되게하기
실패하는지 확인하기 위해 실행
실행하게 만듦
중복 제거
처음 네 단계는 빨리 진행해야한다. 또한, 다섯 번째 단계 없이는 앞의 네 단계도 제대로 되지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
publicclassFranc{ privateint amount;
Franc(int amount) { this.amount = amount; }
Franc times(int multiplier){ returnnew Franc(amount * multiplier); }
publicbooleanequals(Object object){ Franc franc = (Franc) object;
return amount == franc.amount; } }
중복이 엄청나게 많기 때문에, 다음 테스트를 작성하기 전에 이것들을 제거해야한다. equals() 를 일반화하는 것부터 시작하자.
publicbooleanequals(Object object){ Dollar dollar = (Dollar) object; return amount == dollar.amount; }
이렇게 어떻게 리팩토링해야하는지 감이 안오면 삼각측량을 사용하자. 코드와 테스트 사이의 중복을 제거하고 일반적인 해법을 구할 방법이 보이면 그냥 그 방법대로 구현하면 된다. 자, 동일성 문제는 일시적으로 해결했다. 널 값이나 다른 객체들과 비교하는 상황은 일반적이진 않지만 당장은 필요하지 않다. 할일 목록에 적어 두기만 하자.