7장에서 미뤄 둔 방향을 이제 밟는다. Dollar와 Franc을 나누는 진짜 차이는 “통화”뿐이다. 통화가 클래스가 아니라 데이터라면, 언젠가 두 하위 클래스를 하나로 합칠 수 있다. 그 첫걸음으로 currency()를 도입하고, 통화를 문자열 인스턴스 변수로 옮긴다.
통화를 클래스에서 데이터로 옮긴다
먼저 각 하위 클래스가 자기 통화 문자열을 반환하는 currency()를 추상 메서드로 도입한다(빨강→초록). 그다음 그 문자열을 생성자에서 받아 인스턴스 변수에 저장하고, currency()를 Money 한 곳으로 끌어올린다. 그러면 하위 클래스의 currency()가 사라지고 구조가 대칭이 된다.
abstract class Money { protected int amount; static Money dollar(int amount) { return new Dollar(amount); } static Money franc(int amount) { return new Franc(amount); } abstract Money times(int multiplier); abstract String currency(); // 새 추상 메서드 public boolean equals(Object object) { Money money = (Money) object; return amount == money.amount && getClass().equals(money.getClass()); }}class Dollar extends Money { Dollar(int amount) { this.amount = amount; } Money times(int multiplier) { return Money.dollar(amount * multiplier); } String currency() { return "USD"; // Franc은 "CHF"를 반환 }}
두 하위 클래스의 currency()가 “USD”/“CHF” 리터럴을 반환하던 것을, 생성자가 통화 문자열을 받아 필드에 저장하는 방식으로 바꿨다. 팩토리 메서드가 통화를 지정하므로, 이제 Dollar와 Franc의 몸통은 생성자와 times의 팩토리 호출뿐이다 — 거의 같아졌다. 하위 클래스를 없앨 발판이 마련됐다.
판단 기준: 무언가가 타입(클래스)으로 표현돼 있는데 그 차이가 값 하나로 환원된다면, 그것을 데이터로 옮기는 걸 검토한다. 데이터가 된 차이는 런타임에 다루기 쉽고 클래스 폭발을 막는다. 함정: currency()를 곧바로 필드로 만들지 않고, 먼저 추상 메서드로 초록을 확보한 뒤 인스턴스 변수로 리팩터링했다. 이렇게 단계를 나눠야 각 걸음마다 테스트가 지켜 준다.
이 장이 남긴 할 일 목록
통화 개념- Dollar/Franc이 거의 같아졌다 — 하위 클래스를 없앨 수 있을까 → 10장
- times가 여전히 하위 클래스마다 있다
equals(null),equals(다른 객체)처리- 10 (환율 2:1) — 1부의 최종 목표는 아직 남아 있다
1부 화폐 예제의 전반부가 끝났다. Money는 통화를 데이터로 품은 값 객체가 되었고, Dollar와 Franc은 하나로 합쳐지기 직전이다. 다음 장부터 두 하위 클래스를 지우고, 미뤄 둔 덧셈($5 + 10CHF)에 다가선다.
다음장으로 10장