합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.
상속에서 부모 클래스와 자식 클래스 사이의 의존성은 컴파일타임에 해결되지만 합성에서 두 객체의 사이의 의존성은 런타임에 해결된다.
상속 관계 is - a 관계라 부르고 합성 관계는 has - a 관계 라고 부른다. 상속과 합성은 코드 재사용이라는 동일한 목적을 가진다는 점을 제외하면 구현 방법부터 변경을 다루는 방식까지 모든 면에서 차이를 보인다.
합성은 구현에 의존하지 않다는 점에서 상속과 다르다. 합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존한다. 따라서 합성을 이용하면 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화할 수 있기 때문에 변경에 더 안정적인 코드를 얻을 수 있게 된다.
10장에서 상속을 남용했을 때 직면할 수 있는 세 가지 문제점을 살펴봤다.
불필요한 인터페이스 상속 문제
자식 클래스에게는 부적합한 부모 클래스의 오퍼레이션이 상속되기 때문에 자식 클래스 인스턴스의 상태가 불안정해지는 문제. JDK에 포함된 java.util.Properties, java.util.Stack의 예를 살펴봤다.
메서드 오버라이딩의 오작용 문제
자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드의 메서드 호출 방법에 영향을 받는 문제. java.util.HashSet을 상속받은 InstrumentedHashSet을 예로 들었다.
부모 클래스와 자식 클래스의 동시 수정 문제
부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 하는 문제. Playlist를 상속받은 PersonalPlaylist의 예를 살펴봤다.
Properties 크래스에서 상속 관계를 제거하고 Hashtable을 Properties의 인스턴스 변수로 포함시키면 합성 관계로 변경할 수 있다.
public class Properties {
private Hashtable<String, String> properties = new Hashtable<>();
public String setProperty(String key, String value) {
return properties.put(key, value);
}
public String getProperty(String key) {
return properties.get(key);
}
}
클라이언트는 오직 Properties에서 정의한 오퍼레이션만 사용할 수 있다.
Vector를 상속받는 Stack 역시 Vector의 인스턴스 변수를 Stack 클래스의 인스턴스 변수로 선언함으로써 합성 관계로 변경할 수 있다.
public class Stack<E> {
private Vector<E> elements = new Vector<>();
public E push(E item) {
elements.addElement(item);
return item;
}
public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
}
InstrumentedHashSet도 같은 방법을 사용해서 합성 관계로 변경할 수 있다. HashSet 인스턴스를 내부에 포함한 후 HashSet의 퍼블릭 인터페이스에서 제공하는 오퍼레이션들을 이용해 필요한 기능을 구현하면 된다.
public class InstrumentedHashSet<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
addCount++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
여기까지 보면 Properties와 Stack을 변경하던 과정과 동일하게 보일 것이다. 하지만, 다른 점이 한가지 있는데 Properties와 Stack을 합성으로 변경한 이유는 불필요한 오퍼레이션들이 퍼블릭 인터페이스에 스며드는 것을 방지하기 위해서다. 하지만 InstrumentedHashSet의 경우에는 HashSet이 제공하는 퍼블릭 인터페이스를 그대로 제공해야 한다.
HashSet에 대한 구현 결합도는 제거하면서 퍼블릭 인터페이스는 그대로 상속받을 수 있는 방법은 없을까? 다행스럽게도 인터페이스를 사용하면 이 문제를 해결할 수 있다.
public class InstrumentedHashSet<E> implements Set<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
addCount++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean contains(Object o) {
return false;
}
@Override
public Iterator<E> iterator() {
return null;
}
@Override
public Object[] toArray() {
return new Object[0];
}
@Override
public <T> T[] toArray(T[] a) {
return null;
}
@Override
public boolean remove(Object o) {
return false;
}
@Override
public boolean containsAll(Collection<?> c) {
return false;
}
@Override
public boolean retainAll(Collection<?> c) {
return false;
}
@Override
public boolean removeAll(Collection<?> c) {
return false;
}
@Override
public void clear() {
}
}
InstrumentedHashSet의 코드를 보면 Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달한다는 것을 알 수 있다. 이를 포워딩이라 부르고 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드라고 부른다.
안타깝게도 Playlist의 경우에는 합성으로 변경하더라도 가수별 노래 목록을 위해 Playlist와 PersonalPlaylist를 함께 수정해야 하는 문제가 해결되지는 않는다.
즉 새로운 인스턴스 변수가 Person에 추가되면 PersonPlaylist도 수정이 필요하다.
public class PersonPlaylist {
private Playlist playlist = new Playlist();
public void append(Song song) {
playlist.append(song);
}
public void remove(Song song) {
playlist.getTracks().remove(song);
playlist.getSingers().remove(song.getSinger());
}
}
그렇다하더라도 합성이 더 좋은 이유는 향후에 Playlist 내부 구현을 변경하더라도 파급효과를 최대한 PersonPlaylist 내부로 캡슐화할 수 있기 때문이다.
상속으로 인해 결합도가 높아지면 코드를 수정하는 데 필요한 작업의 양이 과도하게 늘어나는 경향이 있다. 가장 일반적인 상황은 작은 기능들을 조합해서 더 큰 기능을 수행하는 객체를 만들어야 하는 경우다. 일반적으로 다음과 같은 두 가지 문제점이 발생한다.
합성을 사용하면 상속으로 인해 발생하는 클래스의 증가와 중복 코드 문제를 간단하게 해결할 수 있다.
10장에서 소개했던 핸드폰 과금 시스템에 새로운 요구사항을 추가해보자. 현재 시스템에는 일반 요금제와 심야 할인 요금제라는 두 가지 종류의 요금제가 존재한다. 새로운 요구사항은 이 두 요금제에 부가 정책을 추가하는 것이다.

부가 정책은 통화량과 무관하게 기본 정책에 선택적으로 추가할 수 있는 요금 방식을 의미한다. 10장에서 살펴봤던 세금을 부과하는 정책이 바로 부가 정책에 해당한다. 앞으로 이 정책을 '세금 정책'이라고 부를 것이다. 부가 정책에는 세금 정책 외에도 최종 계산된 요금에서 일정 금액을 할인해주는 '기본 요금 할인 정책'도 존재한다.

public abstract class Phone {
public double taxRate;
public List<Call> calls = new ArrayList<>();
public Phone(double taxRate) {
this.taxRate = taxRate;
}
public Money calculateFee() {
Money result = Money.ZERO;
for (Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result.plus(result.times(taxRate));
}
abstract protected Money calculateCallFee(Call call);
}
@Getter
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
public RegularPhone(Money amount, Duration seconds, double taxRate) {
super(taxRate);
this.amount = amount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
return amount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@Getter
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
super(taxRate);
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
} else {
return regularAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
}
RegularPhone과 NightlyDiscountPhone의 인스턴스만 단독으로 생성한다는 것은 부가 정책은 적용하지 않고 오직 기본 정책만으로 요금을 계산한다는 것을 의미한다.
만약 일반 요금제에 세금 정책을 조합해야 한다면 어떻게 해야 할까? 가장 간단한 방법은 RegularPhone 클래스를 상속받은 TaxableRegularPhone 클래스를 추가하는 것이다.
public class TaxableRegularPhone extends RegularPhone {
private double taxRate;
public TaxableRegularPhone(Money amount, Duration seconds, double taxRate) {
super(amount, seconds);
this.taxRate = taxRate;
}
@Override
public Money calculateFee() {
Money fee = super.calculateFee();
return fee.plus(fee.times(taxRate));
}
}
부모 클래스의 메서드를 재사용하기 위해 super 호출을 사용하면 원하는 결과를 쉽게 얻을 수는 있지만 자식 클래스와 부모 클래스 사이의 결합도가 높아지고 만다. 결합도를 낮추는 방법은 자식 클래스가 부모 클래스의 메서드를 호출하지 않도록 부모 클래스에 추상 메서드를 제공하는 것이다.
먼저 Phone 클래스에 새로운 추상 메서드인 arterCalculated를 추가하자.
public abstract class Phone {
public List<Call> calls = new ArrayList<>();
public Money calculateFee() {
Money result = Money.ZERO;
for (Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return afterCalculated(result);
}
abstract protected Money calculateCallFee(Call call);
abstract protected Money afterCalculated(Money fee);
}
@Getter
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
public RegularPhone(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
return amount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
@Getter
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
} else {
return regularAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
위 코드에서 알 수 있는 것처럼 부모 클래스에 추상 메서드를 추가하면 모든 자식 클래스들이 추상 메서드를 오버라이딩해야 하는 문제가 발생한다.
모든 추상 메서드의 구현이 동일하다는 사실에도 주목하기 바란다. 유연성은 유지하면서 중복 코드를 제거할 수 있는 방법은 Phone에서 afterCalculated 메서드에 대한 기본 구현을 함께 제공하는 것이다.
public abstract class Phone {
public List<Call> calls = new ArrayList<>();
public Money calculateFee() {
Money result = Money.ZERO;
for (Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return afterCalculated(result);
}
protected Money afterCalculated(Money fee) {
return fee;
}
protected abstract Money calculateCallFee(Call call);
}
public class TaxableRegularPhone extends RegularPhone {
private double taxRate;
public TaxableRegularPhone(Money amount, Duration seconds, double taxRate) {
super(amount, seconds);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRate));
}
}
public class TaxableNightlyDiscountPhone extends NightlyDiscountPhone {
private double taxRate;
public TaxableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
super(nightlyAmount, regularAmount, seconds);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRate));
}
}
그림 11.3은 Phone의 상속 계층에 세금 정책을 추가한 상속 계층을 다이어그램으로 표현한 것이다.

문제는 TaxableNightlyDiscountPhone과 TaxableRegularPhone 사이에 코드를 중복했다는 것이다. 두 클래스를 살펴보면 대부분의 코드가 거의 동일하다.
이번에는 두 번째 부가 정책인 기본 요금 할인 정책을 Phone의 상속 계층에 추가해보자. 기본 요금 할인 정책이란 매달 청구되는 요금에서 고정된 요금을 차감하는 부가 정책을 가리킨다. 예를 들어, 매달 1000원을 할인해주는 요금제가 있다면 이 요금제에는 부가 정책으로 기본 요금 할인 정책이 조합돼 있다고 볼 수 있다.
public class RateDiscountableRegularPhone extends RegularPhone {
private Money discountAmount;
public RateDiscountableRegularPhone(Money amount, Duration seconds, Money discountAmount) {
super(amount, seconds);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
public class RateDiscountableNightlyDiscountPhone extends NightlyDiscountPhone {
private Money discountAmount;
public RateDiscountableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, Money discountAmount) {
super(nightlyAmount, regularAmount, seconds);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}

하지만 이번에도 RateDiscountableRegularPhone, RateDiscountableNightlyDiscountPhone 클래스 사이에 중복 코드를 추가했다.
부가 정책은 자유롭게 조합할 수 있어야 하고 적용되는 순서 역시 임의로 결정할 수 있어야 한다. 이 요구사항에 따르면 앞에서 구현한 세금 정책과 기본 요금 할인 정책을 함께 적용하는 것도 가능해야 하고, 세금 정책을 적용한 후에 기본 요금 할인 정책을 적용하거나 기본 요금 할인 정책을 적용한 후에 세금 정책을 적용하는 것도 가능해야 한다.
상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다.
public class TaxableAndRateDiscountableRegularPhone extends TaxableRegularPhone {
private Money discountAmount;
public TaxableAndRateDiscountableRegularPhone(Money amount, Duration seconds, double taxRate, Money discountAmount) {
super(amount, seconds, taxRate);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).minus(discountAmount);
}
}
TaxableAndRateDiscountableRegularPhone의 afterCalculated 메서드는 부모 클래스인 TaxableRegularPhone의 afterCalculated를 호출해서 세금이 부과된 요금을 계산한 후 기본 요금 할인 정책을 적용한다. 따라서 세금을 부과하고 나서 기본 요금 할인을 적용하는 순서로 정책을 조합할 수 있다.
표준 요금에 기본 요금 할인 정책을 먼저 적용한 후 세금을 나중에 부과하고 싶다면 RateDiscountableRegularPhone을 RateDiscountableAndTaxableRegularPhone 클래스를 추가하면 된다.
public class RateDiscountableAndTaxableRegularPhone extends RateDiscountableRegularPhone {
private double taxRate;
public RateDiscountableAndTaxableRegularPhone(Money amount, Duration seconds, Money discountAmount, double taxRate) {
super(amount, seconds, discountAmount);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).plus(fee.times(taxRate));
}
}
TaxableAndDiscountableNightlyDiscountPhone 클래스는 심야 할인 요금제의 계산 결과에 세금 정책을 적용한 후 기본 요금 할인 정책을 적용하는 케이스를 구현한다.
public class TaxableAndDiscountableNightlyDiscountPhone extends TaxableNightlyDiscountPhone {
private Money discountAmount;
public TaxableAndDiscountableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate, Money discountAmount) {
super(nightlyAmount, regularAmount, seconds, taxRate);
this.discountAmount = discountAmount;
}
}
마지막으로 RateDiscountableAndTaxableNightlyDiscountPhone 클래스는 심야 할인 요금제의 계산 결과에 기본 요금 할인 정책을 적용 후 세금 정책을 적용한다.

그림 11.5에 새로운 기본 정책을 추가하면 그에 따른 조합 가능한 부가 정책의 수만큼 새로운 클래스를 추가해야 한다.

이처럼 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스가 추가된다.
상속 관계는 컴파일타임에 겨렁되고 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다. 따라서 여러 기능을 조합하는 설계에 상속을 사용하면 조합 별로 클래스를 추가해야 한다.
합성 관계는 컴파일타임 의존성과 런타임 의존성을 다르게 만들 수 있다. 이런 클래스 폭발 분제를 해결하기 위해 합성을 사용하는 이유는 런타임에 객체 사이의 의존성을 자유롭게 변경할 수 있기 때문이다.
가장 먼저 해야 할 일은 각 정책을 별도의 클래스로 구현하는 것이다. 조합을 사용하려면 핸드폰이라는 개념으로부터 요금 계산 방법이라는 개념을 분리해야 한다.
먼저 기본 정책과 부가 정책을 포괄하는 RatePolicy 인터페이스를 추가하자. RatePolicy는 Phone을 인자로 받아 계산된 요금을 반환하는 calculateFee 오퍼레이션을 포함하는 간단한 인터페이스다.
public interface RatePolicy {
Money calculateFee(Phone phone);
}
기본 정책부터 구현하자.
public abstract class BasicRatePolicy implements RatePolicy {
@Override
public Money calculateFee(Phone phone) {
Money result = Money.ZERO;
for (Call call : phone.getCalls()) {
result.plus(calculateCallFee(call));
}
return result;
}
protected abstract Money calculateCallFee(Call call);
}
BasicRatePolicy의 기본 구현은 상속 버전의 Phone 클래스와 거의 동일하다.
일반 요금제를 구현하자.
public class RegularPolicy extends BasicRatePolicy {
private Money amount;
private Duration seconds;
public RegularPolicy(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
심야 할인 요금제를 구현하자.
public class NightlyDiscountPolicy extends BasicRatePolicy {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NightlyDiscountPolicy(Money nightlyAmount, Money regularAmount, Duration seconds) {
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
이제 기본 정책을 이용해 요금을 계산할 수 있도록 Phone을 수정하자.
@Getter
public abstract class CompositionPhone {
private RatePolicy ratePolicy;
public List<Call> calls = new ArrayList<>();
public CompositionPhone(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}
public List<Call> getCalls() {
return Collections.unmodifiableList(calls);
}
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
상속 Phone과 조합 Phone을 구분하기 위해 CompositionPhone로 클래스명 변경
Phone 내부에 RatePolicy에 대한 참조자가 포함돼 있다는 것에 주목하라. 이것이 바로 합성이다. Phone이 다양한 요금 정책과 협력할 수 있어야 하므로 요금 정책의 타입인 RatePolicy라는 인터페이스로 정의돼 있다는 것에도 주목하라. Phone은 이 컴파일타임 의존성을 구체적인 런타임 의존성으로 대체하기 위해 생성자를 통해 RatePolicy의 인스턴스에 대한 의존성을 주입 받는다.

일반 요금제의 규칙에 따라 통화 요금을 계산하고 싶으면 다음과 같이 Phone과 BaiscRatePolicy의 인스턴스를 합성하면 된다.
Phone phone = new Phone(new RegularPolicy(Money.wons(10), Duration.ofSeconds(10)));
심야 할인 요금제의 규칙에 따라 통화 요금 계산
Phone phone = new Phone(new NightlyDiscountPolicy(Money.wons(5), Money.wons(10), Duration.ofSeconds(10)));
합성을 사용하면 Phone과 연결되는 RatePolicy 인터페이스의 구현 클래스가 어떤 타입인지에 따라 요금을 계산하는 방식이 달라진다.
일반 요금제를 적용한 경우 생성된 인스턴스 관계를 살펴보자. 그림 11.8에서 알 수 있는 것처럼 컴파일 시점의 Phone 클래스와 RatePolicy 인터페이스 사이의 관계가 런타임에 Phone 인스턴스와 RegularPolicy 인스턴스 사이의 관계로 대체됐다는 것을 알 수 있다.

지금부터 할 일은 여기에 부가 정책을 추가하는 것이다. 부가 정책은 기본 정책에 대한 계산이 끝난 후에 적용된다.

만약 일반 요금제에 기본 요금 할인 정책을 적용한 후에 세금 정책을 적용해야 한다면 아래와 같은 순서로 인스턴스를 연결해야 한다.

그림 11.9와 그림 11.10은 다음의 두 가지 제약에 따라 부가 정책을 구현해야 한다는 사실을 잘 보여준다.
부가 정책을 AdditionalRatePolicy 추상 클래스로 구현하자.
public abstract class AdditionalRatePolicy implements RatePolicy {
private RatePolicy next;
public AdditionalRatePolicy(RatePolicy next) {
this.next = next;
}
@Override
public Money calculateFee(CompositionPhone compositionPhone) {
Money fee = next.calculateFee(compositionPhone);
return afterCalculated(fee);
}
abstract protected Money afterCalculated(Money fee);
}
Phone의 입장에서 AdditinalRatePolicy는 RatePolicy의 역할을 수행하기 때문에 RatePolicy 인터페이스를 구현한다. 또한 다름 요금 정책과 조합될 수 있도록 RatePolicy 타입의 인스턴스를 포함한다. 그리고 런타임 의존성으로 쉽게 대체할 수 있도록 RatePolicy 타입의 인스턴스를 인자로 받는 생성자를 제공한다.
세금 정책을 구현하자.
public class TaxablePolicy extends AdditionalRatePolicy {
private double taxRatio;
public TaxablePolicy(RatePolicy next, double taxRatio) {
super(next);
this.taxRatio = taxRatio;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRatio));
}
}
기본 할인 정책도 추가하자.
public class RateDiscountablePolicy extends AdditionalRatePolicy {
private Money discountAmount;
public RateDiscountablePolicy(RatePolicy next, Money discountAmount) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}

상속 기반의 설계는 부가 정책 추가를 위해 계속 클래스를 추가해야 했다. 합성을 기반으로 한 설계는 이 문제를 간단하게 해결할 수 있다. 만약 고정 요금제만 필요하다면 고정 요금제를 구현한 클래스 '하나'만 추가하면 된다.

약정 할인 정책이라는 새로운 부가 정책도 마찬가지이다.

더 중요한 것은 요구사항을 변경할 때 오직 하나의 클래스만 수정해도 된다는 것이다.