두 times() 구현이 거의 비슷하다. 하지만 완전히 동일하지 않다.
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return Money.franc(amount * multiplier); } }
1 2 3 4 5 6 7 8 9 class Dollar extends Money { Dollar(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return Money.dollar(amount * multiplier); } }
팩토리 메서드를 인라인 시키면 어떨까 ?
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Franc(amount * multiplier, "CHF" ); } }
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Dollar(amount * multiplier, "USD" ); } }
바로 전 장에서 팩토리 메서드를 호출하는 것으로 바꿨었는데.. 실망스러운 일이다. Franc 에서는 인스턴스 변수 currency 가 항상 ‘CHF’ 이므로,
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Franc(amount * multiplier, currency); } }
1 2 3 4 5 6 7 8 9 class Dollar extends Money { Dollar(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Dollar(amount * multiplier, currency); } }
Franc 를 가질지, Money 를 가질지 정말 중요한 사실인가 ? 고민하는 대신 그냥 수정하고 테스트를 돌려보자.
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Money(amount * multiplier, currency); } }
컴파일러가, Money 를 Concrete 클래스로 바꿔야한닫고 한다.
1 2 3 4 5 6 7 class Money { ... Money times (int multiplier) { return null ; } }
빨간 막대다. Expected :me.jko.tddstudy.Franc@262b2c86 Actual :me.jko.tddstudy.Money@371a67ec 더 나은 메세지를 보기 위해 toString() 을 정의하자.
1 2 3 4 public String toString () { return amount + " " + currency; }
테스트도 없이 코드를 작성했네 ? 테스트를 먼저 작성하는게 맞다. 하지만.
화면에 나타나는 결과를 확인하려던 참이었다.
toString 은 디버그 출력으로만 쓰이기 떄문에, 잘못 구현됨으로 인해 얻게 될 리스크가 적다.
이미 빨간 막대 상태에서는 새로운 테스트를 작성하지 않는게 좋다.
이제 에러 메세지에, expected: me.jko.tddstudy.Franc@262b2c86<10 CHF> but was: me.jko.tddstudy.Money@371a67ec<10 CHF> 조금 나아졌지만, 혼란스럽다. 답은 맞았는데 클래스가 다르다. Franc 대신 Money 가 나왔다. 문제는 여기에 있다.
1 2 3 4 5 6 public boolean equals (Object object) { Money money = (Money) object; return amount == money.amount && getClass().equals(money.getClass()); }
정말로 검사해야할 것은 클래스가 같은지가 아니라, currency 가 같은지 여부다. 빨간 막대 상황에서는 테스트를 추가로 작성하고 싶지 않다. 하지만 지금은 실제 모델 코드를 수정하려고 하는중이고 테스트 없이는 모델 코드를 수정할 수 없다. 변경된 코드를 되돌려서 다시 초록 막대 상태로 돌아가야한다.
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Franc(amount * multiplier, currency); } }
다시 초록 막대이다. 우리 상황은 Franc(10, ‘CHF’) 와 Money(10, ‘CHF’) 가 서로 같기를 바라지만, 사실 그렇지 않다고 보고된 것이다. 이걸 그대로 테스트 해보자.
1 2 3 4 @Test public void testDifferentClassEquality () { assertTrue(new Money(10 , "CHF" ).equals(new Franc(10 , "CHF" ))); }
예상 대로 실패한다. equals() 는 클래스가 아니라, currency 를 비교해야한다.
1 2 3 4 5 6 7 public boolean equals (Object object) { Money money = (Money) object; return amount == money.amount && currency().equals(money.currency()); }
이제 Franc.times() 에서 Money 를 반환해도 테스트가 여전히 통과한다.
1 2 3 4 5 6 7 8 9 class Franc extends Money { Franc(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Money(amount * multiplier, currency); } }
Dollar.times() 에도 적용하자.
1 2 3 4 5 6 7 8 9 class Dollar extends Money { Dollar(int amount, String currency) { super (amount, currency); } Money times (int multiplier) { return new Money(amount * multiplier, currency); } }
잘 돈다. 이제 두 구현이 동일해졌다. 위로 올리자.
1 2 3 4 Money times (int multiplier) { return new Money(amount * multiplier, currency); }
이제 아무것도 안 하는 하위 클래스들을 제거할 수 있겠다.
테스트 주도 개발 <켄트 벡>