7장에서 미뤄 둔 방향을 이제 밟는다. Dollar와 Franc을 나누는 진짜 차이는 “통화”뿐이다. 통화가 클래스가 아니라 데이터라면, 언젠가 두 하위 클래스를 하나로 합칠 수 있다. 그 첫걸음으로 currency()를 도입하고, 통화를 문자열 인스턴스 변수로 옮긴다.

통화를 클래스에서 데이터로 옮긴다

먼저 각 하위 클래스가 자기 통화 문자열을 반환하는 currency()를 추상 메서드로 도입한다(빨강→초록). 그다음 그 문자열을 생성자에서 받아 인스턴스 변수에 저장하고, currency()를 Money 한 곳으로 끌어올린다. 그러면 하위 클래스의 currency()가 사라지고 구조가 대칭이 된다.

Refactoring Step 빨강→초록 — currency()를 하위 클래스가 반환한다
1/2
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장