지금 plustimes는 Money에만 있다. 그래서 (5달러 + 10프랑) + 5달러처럼 식에 다시 더하거나, 식 전체에 곱하는 일을 할 수 없다. 진짜 추상화라면 Money든 Sum이든 똑같이 더하고 곱할 수 있어야 한다. Expression을 완성할 차례다.

목표는 대칭이다. Expression.plus는 Expression을 반환하고, Expression.times도 Expression을 반환한다. Money와 Sum이 이 둘을 각자 구현하면, 식을 자유롭게 조합해도 결과는 언제나 또 다른 식이다 — 닫힌 대수(closed algebra).

사이클: Expression.plus → Expression.times

Refactoring Step Expression에 plus를 올리고 Sum.plus 구현
1/2
interface Expression {    Money reduce(Bank bank, String to);    Expression plus(Expression addend);}class Money implements Expression {    public Expression plus(Expression addend) {        return new Sum(this, addend);    }    // reduce, times, equals 는 그대로}class Sum implements Expression {    Expression augend;    Expression addend;    public Expression plus(Expression addend) {        return new Sum(this, addend);    }    public Money reduce(Bank bank, String to) {        int amount = augend.reduce(bank, to).amount                   + addend.reduce(bank, to).amount;        return new Money(amount, to);    }}// Sum도 plus를 갖는다. (5+10프랑)+5 = 15달러. testSumPlusMoney 초록.// augend/addend가 Money에서 Expression으로 넓어졌다.

Sum.times가 곱을 각 항에 분배하는 모습(augend.times(m), addend.times(m))은 15장의 Sum.reduce가 환산을 각 항에 위임하던 것과 판박이다. Sum은 언제나 자기 일을 하지 않고 항에게 넘긴 뒤 결과를 조립한다 — 이 재귀적 위임이 복합 식(Sum 안의 Sum)까지 자연스럽게 처리한다.

Money와 Sum이 이제 완전히 대칭이다. 둘 다 reduce, plus, times를 갖고, 어떤 조합을 만들어도 결과 타입은 Expression이다. 클라이언트는 눈앞의 식이 단일 금액인지 복합 합인지 알 필요가 없다 — Expression이라는 한 단어로 충분하다. 1장의 Dollar.times가 int를 반환하던 것에서 여기까지, 추상화는 테스트가 요구할 때마다 한 걸음씩 자라 마침내 닫혔다.

판단 기준: 연산의 결과 타입을 피연산자 타입과 같게(Expression → Expression) 맞추면 연산을 무한히 조합할 수 있다. 이 “닫힘”이 추상화가 완성됐다는 신호다. 함정: Money에만 있던 메서드를 인터페이스로 올릴 때 반환 타입을 구체 타입(Money)으로 두면 조합이 막힌다. 추상 타입을 반환해야 대칭이 닫힌다.

이 장이 남긴 할 일 목록

  • fiveBucks/tenFrancs를 Expression 타입으로
  • Sum.plus / Sum.times
  • Expression 추상화 완성 — Money·Sum 대칭
  • Pair.hashCode 개선 (남은 기술 부채)
  • 1부 회고 → 17장

다음장으로 17장