상속은 클라스 사이의 정적인 관계인 것에 비해, 합성은 객체 사이의 동적인 관계다.
따라서, 상속은 변경이 불가능하지만, 합성은 실행 시점에 동적으로 변경할 수 있다.
사용의 용이함을 가진 상속보다, 변경에 유연한 대처를 가진 합성을 선택해라.
[코드 재사용을 위해서는] 객체 합성이 클래스의 상속보다 더 좋은 방법이다. [GOF94].
객체지향 시스템에서 기능을 재사용할 수 있는 가장 대표적인 기법은 클래스 상속(class inheritance)과 객체 합성(object composition)이다. ... 클래스 상속은 다른 클래스를 이용해서 한 클래스의 구현을 정의하는 것이다. 서브 클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)이라고 부른다. 화이트박스라는 말은 가시성때문에 나온 말이다. 상속을 받으면 부모 클래스의 내부가 자식 클래스에 공개되기 때문에 화이트박스인 셈이다.
객체 합성은 클래스 상속의 대안이다. 새로운 기능을 위해 객체들을 합성한다. 객체를 합성하려면 합성할 객체들의 인터페이스를 명확하게 정의해야만 한다. 이런 스타일의 재사용을 블랙박스 재사용(balck-box reuse)이라고 하는데, 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되기 때문이다.[GOF67].
java.util.Properties, java.util.Stackjava.util.HashSet을 상속받은 InstrumentHashSetPlaylist를 상속받은 PersonalPlaylist합성을 사용하면 상속이 초래한 문제점들을 해결할 수 있다.
상속을 합성으로 변경해 문제를 해결해보자!
상속을 합성으로 변경하는 방법
1. 자식 클래스에 선언된 상속 관계를 제거
2. 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 변수로 선언
java.util.Propertiespublic class Properties { // 1. 상속 제거
// 2. 부모 클래스 인스턴스 변수 선언
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);
}
}
Hashtable(부모)의 오퍼레이션이 Properties (자식)클래스의 퍼블릭 인터페이스를 오염시키지Properties(자식)클래스에 정의된 오퍼레이션만 사용할 수 있다.java.util.Stackpublic 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);
}
}
Stack의 퍼블릭 인터페이스에 불필요한 Vector의 오퍼레이션이 포함되지 않는다.Vector를 통해 Stack의 규칙을 어기고 잘못 사용할 수 있을 가능성을 깔끔하게 제거했다.InstrumentedHashSetInstrumentedHashSet은 앞서 2개의 예시와 달리, HashSet(부모)가 제공하는 퍼블릭 인터페이스를 그대로 제공해야 한다.
HashSet에 대한 구현 결합도는 제거하면서, 퍼블릭 인터페이스는 그대로 상속받을 수는 없을까?HashSet이 제공하는 Set인터페이스를 실체화 한다. (퍼블릭 인터페이스 상속)HashSet의 인스턴스를 합성한다. (구현 결합도 제거)public class InstrumentedHashSet<E> implements Set<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
@Override
public boolean add(E e) {
addCount++;
return set.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
@Override public boolean remove(Object e) {return set.remove(o);}
@Override public void clear() {set.clear();}
@Override public boolean equals(Object o) {return set.equals(o);}
...
}
Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달한다. 이를 포워딩(forwarding)이라고 한다.
public class PersonalPlaylist {
private Playlist playlist = new Palylist();
public void append(Song song) {
playlist.append(song);
}
public void remove(Song song) {
playlist.getTrack().remove(song);
playlist.getSingers().remove(Song.getSinger());
}
}
Playlist와 PersonalPlaylist를 함께 변경해야 한다는 문제는 해결되지 않는다.Playlist의 내부 구현 변경에 의한 파급효과를 PersonalPlaylist 내부로 캡슐화할 수 있기 때문이다.playlist의 퍼블릭 인터페이스만 참고하고 있으므로, 캡슐화가 되어있다.)몽키 패치(Monkey Patch)
- 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것
- ex) 루비의 열린 클래스(Open Class), C#의 확장 메서드(Extension mehtod), 스칼라의 암시적 변환(implicit conversion)
- 직접적으로
Playlist를 수정할 권한이 없거나 소스코드가 존재하지 않더라도, 몽키 패치가 지원되는 환경이라면Playlist에 직접remove메서드를 추가하는 것이 가능하다.- 자바에서는 언어적으로 몽키 패치를 지원하지 않으므로, 바이트 코드를 직접 변환하거나 AOP(Aspect-Oriented Programming)을 이용해 몽키 패치를 구현하고 있다.
상속으로 인한 결합도 증가는, 코드 수정에 요구되는 작업 양이 과도하게 늘어나는 경향이 있다.
작은 기능들을 조합해 더 큰 기능을 수행하는 객체를 만들어야 하는 경우에 빈번하게 발생한다.
합성을 이용하면 상속으로 인해 발생하는 문제를 간단하게 해결할 수 있다.


RegularPhone과 NightlyDiscountPhone은 Phone을 상속받는다.RegularPhone과 NightlyDiscountPhone 인스턴스만 단독으로 생성하는 것은 부가 정책은 적용하지 않고 기본 정책만으로 요금을 계산함을 의미한다.public abstarct class Phone {
private List<Call> calls = new ArrayList<>();
public Money calcualteFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result;
}
abstract protected Money calculateCallFee(Call call);
}
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
public RegularPhone(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = secondse;
}
@Override
protected Money calcualteCallFEe(Call call) {
return amount.tiimes(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
public class NightlyDiscountPhone extends Phone {
prviate static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NgithlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Overrid
protected Money caculateCallFee(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());
}
}
TaxableRegularPhone: RegularPhone 상속calculateFee를 오버라이딩 한 후 super 호출을 통해 일반요금제 규칙으로 계산된 요금을 구하고, 거기에 추가적으로 세금을 부과한다.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 사용은 부모-자식간의 결합도를 높인다.afterCalculated를 추가한다.afterCalculated를 오버라이딩 해 계산된 요금에 적용할 작업을 추가한다.public abstract class Phone {
private 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 abstract Money calculateCallFee(Call call); // 추상 메서드 제공
protected abstract Money afterCalculated(Money fee);
}
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
public RegularPhone(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
@Override
purotected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSEconds());
}
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
public class NightlyDiscountPhone extends Phone {
privat static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NgithlyDiscountPhone(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());
} else {
return regularAmount.times(call.getDuration().getSeconds / seconds.getSeconds());
}
}
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
Phone에서 afterCalculatd에 대한 기본 구현을 함께 제공할 수 있다.RegularPhone과 NightlyDiscountPhone에서는 afterCalculated 메서드를 오버라이딩 하지 않아도 된다.afterCalculatd와 같은 메서드를 훅 메서드라고 부른다.public abstract class Phone {
...
protected Money afterCalculated(Money fee) {
return fee;
}
protected abstract Monoey calculateCallFee(Call call);
}
📌 추상 메서드와 훅 메서드
- 추상메서드 (개방-폐쇄 원칙을 만족하는 설계 방법)
- 부모 클래스에 새로운 추상 메서드를 추가하고, 부모 클래스의 다른 메서드 안에서 호출
- 자식 클래스는 추상 메서드를 오버라이딩하고 자신만의 로직을 구현함으로써, 부모 클래스에서 정의한 플로우에 개입할 수 있게 된다.
- 예시에서
calcualtedFee,afterCalculated- 추상 메서드의 단점: 모든 자식 클래스가 추상 메서드를 오버라이딩해야 한다.
- 대부분의 자식 클래스가 동일한 방식으로 추상메서드를 구현하는 경우, 상속 계층 전반에 걸쳐 중복 코드가 존재하게 된다.
- 이런 경우, 편의를 위해 기본 구현을 제공할 수 있는데, 이러한 메서드를 훅 메서드(hook method)라고 부른다.
부모 클래스의 이름을 제외한 TaxableRegularPhone과 TaxableNightlyDiscountPhone 사이의 코드 대부분이 중복된다.
자바를 비롯한 대부분의 객체지향 언어는 단일 상속만 지원하므로, 상속으로 인해 발생하는 중복 코드 문제 해결이 쉽지 않다.
public class TaxableRegularPhone extends RegularPhone {
private double taxRate;
public TaxaleRegularPhone(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));
}
}

RegularPhone 인스턴스 생성TaxableRegularPhone 인스턴스 생성NightlyDiscountPhone 인스턴스 생성TaxableNightlyDiscountPhone 인스턴스 생성 RegularPhone 상속public class RateDiscountableRegularPhone extends RegularPhone {
private Money discountAmount;
public RateDiscountableRegularPhone(Money amount, Duration seconds, Money discountAmount) {
super(amound, seconds);
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
public class RateDiscountableNightlyDiscountPhone extends NightlyDiscountPhone {
private Money discountAmount;
public RateDiscountableNightlyDiscountPhone(
Money regularAmount,
Duration seconds,
Money discountAmount
) {
super(nightlyAmount, regularAmount, seconds);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(money fee) {
return fee.minus(discountAmount);
}

// 일반요금제 + 세금 정책 + 기본요금 할인정책
public class TaxableAndRateDiscountableRegularPhone extends TaxableRegularPhone {
privte Money discountAmount;
...
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).minus(discountAmount);
}
}
// 일반요금제 + 기본요금 할인정책 + 세금 정책
public class RateDiscountableAndTaxableRegularPhone extends RateDiscountableRegularPhone {
private Money taxRate;
...
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).plust(fee.times(taxRate));
}
}
// 심야할인 요금제 + 세금 정책 + 기본요금 할인 정책
public class TaxableAndDiscountableNightlyDiscountPhone extends TaxableNightlyDiscountPhone {
private Money discountAmount;
...
@Override
protected Money afterCalculated(Money fee) {
return super.afterClauclated(fee).minus(discountAmount);
}
}
// 심야할인 요금제 + 기본요금 할인 정책 + 세금 정책
public class RateDiscountalbeAndTaxableNightlyDiscountPhone extends RateDiscountableNightlyDiscountPhone {
private double taxRate;
...
@Override
protected Money afterClauclated(Money fee) {
return super.afterCalculated(fee).plus(fee.times(taxRate));
}
}

FixedRatePhone) 기본 정책이 추가되는 경우
합성을 사용하면 구현 시점에 정책들의 관계를 고정시킬 필요가 없으며, 실행 시점에 정책들의 관계를 유연하게 변경할 수 있게 된다.
합성은 조합을 구성하는 요소들을 개별 클래스로 구현한 후 실행 시점에 인스턴스를 조립하는 방법이라고 할 수 있다.
합성의 가장 큰 장점은 컴파일 의존성에 속박되지 않고 다양한 방식의 런타임 의존성을 구성할 수 있다는 점이다.
컴파일타임 의존성과 런타임 의존성 사이의 거리가 멀면 멀수록 설계의 복잡도가 상승하므로, 코드를 이해하는 것은 어려워질 수 있다.
대부분의 경우 단순한 설계가 정답이지만, 변경에 따르는 고통이 복잡성으로 인한 혼란을 넘어서고 있다면 유연성을 선택하는 것이 현명한 판단일 수 있다.
Phone을 인자로 받아 계산된 요금을 반환하는 calculateFee 오퍼레이션을 포함public interface RatePolicy {
Money calculateFee(Phone 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);
}
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 NgithlyDiscountPolicy extends BasicRatePolicy {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NgithlyDiscountPhone(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());
}
}
RatePolicy에 대한 참조자가 포함.RatePolicy : 다양한 요금 정책과 협력가능하도록 요금 정책의 타입이 인터페이스로 정의되어 있다.Phone 생성자: 컴파일타임 의존성을 구체적인 런타임 의존성으로 대체하기 위해 생성자를 통해 RatePolicy 인스턴스에 대한 의존성을 주입받는다.public calss Phone {
private RatePlicy ratePolicy; // 합성
private List<Call> calls = new ArrayList<>();
public Phone(RatePlolicy ratePolicy) { // 외부 의존성 주입
this.ratePolicy = ratePolicy;
}
public List<Call> getCall() {
return Collections.unmodifiableList(calls);
}
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
합성을 사용하면 Phone과 연결되는 RatePolicy의 구현 클래스가 어떤 타입인지에 따라 요금을 계산하는 방식이 달라진다.
// 일반 요금제 규칙으로 통화 요금을 계산하는 경우
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)));


RegularPolicy의 계산이 끝나고 Phone에게 반환되기 전에 적용되어야 한다.

Phone의 입장에서는 자신이 기본 정책의 인스턴스에게 메시지를 전송하고 있는지, 부가 정책의 인스턴스에게 메시지를 전송하고 있는지를 몰라야 한다. 다시 말해서 기본 정책과 부가 정책은 협력 안에서 동일한 '역할'을 수행해야 한다. RatePolicy 인터페이스를 구현해야 함을 의미한다.RatePolicy 인터페이스를 구현해야 함.RatePolicy를 합성할 수 있어야 함.Phone의 입장에서 AdditionalRatePolicy는 RatePolicy의 역할을 수행하므로 인터페이스를 구현한다.
또한 또 다른 요금 정책과 조합될 수 있도록 RatePolicy 타입의 next라는 이름을 가진 인스턴스 변수를 내부에 포함한다.
public abstract class AdditionalRatePolicy implements RatePolicy { // 1.
private RatePolicy next; // 2.
public AdditionalRatePolicy(RatePolicy next) {
this.next = next;
}
@Override
public Money calculateFee(Phone phone) {
Money fee = next.calculateFee(phone);
return afterCalcualted(fee);
}
abstract protected Money afterCalculated(Money fee);
}
public class TaxablePolicy extends AdditionalRatePolicy {
private double taxRatio;
public TaxablePolicy(double taxRatio, RatePolicy next) {
super(next);
this.taxRatio = taxRatio;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRatio));
}
}
public class RateDiscountablPolicy extends AdditionalRatePolicy {
private Money discountAmount;
public RatioDiscountablePolicy(Money discountAmount, RatePolicy next) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}

// 일반 요금제 + 세금 정책
Phone phone = new Phone(
new TaxablePolicy(0.05,
new RegularPolicy(...));
// 일반 요금제 + 기본요금 할인정책 + 세금 정책
Phone phone = new Phone(
new TaxablePolicy(0.05,
new RegularDiscountPolicy(Money.wons(1000),
new RegularPolicy(...)));
// 일반 요금제 + 세금 정책 + 기본요금 할인정책
Phone phone = new Phone(
new RateDiscountPolicy(Money.wons(1000),
new TaxablePolicy(0.05,
new RegularPolicy(...)));
// 심야할인 요금제 + 세금 정책 + 기본요금 할인정책
Phone phone = new Phone(
new RateDiscountPolicy(Money.wons(1000),
new TaxablePolicy(0.05,
new NightlyDiscountPolicy(...)));
상속을 이용한 설계보다 복잡하고, 정해진 규칙에 따라 객체를 생성하고 조합해야 하므로, 처음에는 코드를 이해하기 어려울 수 있다.
그러나 객체를 조립해 사용하는 방식이, 상속을 사용한 방식보다 더 예측 가능하고 일관성있다.
합성의 진정한 진가는 새로운 클래스의 추가나 수정의 시점에 비로소 알 수 있다.
FixedRatePolicy)
AggrementDiscountablePolicy)
오직 하나의 클래스를 추가하고, 런타임에 필요한 정책들을 조합해 원하는 기능을 얻을 수 있다.
그렇다면 상속은 사용하면 안 되는 것일까?
상속에는 2가지 상속이 있다.
이번 장에서 살펴본 상속에 대한 모든 단점들은 구현 단점에 국한되며, 인터페이스 상속을 사용해야 한다.
코드를 재사용하면서도 납득할만한 결합도를 유지하는 것.
합성은 객체의 구체적인 구현이 아니라 추상적인 인터페이스에 의존한다.
상속과 클래스를 기반으로 하는 재사용 방법을 사용하면 클래스의 확장과 수정을 일관성있게 표현할 수 있는 추상화의 부족으로 인해 변경하기 어려운 코드를 얻게 된다.
코드를 다른 코드 안에 유연하게 섞어 넣을 수 있다면 믹스인이라고 부를 수 있다.
스칼라 언어에서 제공하는 트레이트(trait)를 이용해 믹스인을 구현해보자.
abtract class BasicRatePolicy {
def calculateFee(phone : Pone): Money =
phone.calls.map(calcualteCallFEe(_)).reduce(_ + _)
protected def calcualteCallFee(call: Call): Money;
}
class RegularPolicy(val amount: Money, val seconds: Duration) extends BasicRatePolicy {
override protected def calculateCallFee(call: Call): Money =
amount * (call.duration.getSceconds / seconds.getSeconds)
}
class NightlyDiscountPolicy {
val nightlyAmount: Money,
val regularAmount: Money,
val seconds: Duration) extends BasicRatePolicy {
override protected def calcualteCallFee(call: Call): Money =
if (call.from.getHour >= NightlyDiscountPolicy.LateNightHour) {
nightlyAmount * (call.duration.getSeconds / seconds.getSeconds)
} else {
regularAmount * (call.duration.getSeconds / seconds.getSeconds)
}
}
}
object NightlyDiscountPolicy {
val LateNightHour: Integer = 22
}
스칼라에서는 다른 코드와 조합해서 확장하는 기능을 트레이트로 구현할 수 있다.
trait TaxablePolicy extends BasicRatePolicy {
def taxRate: Double
override def calculateFee(phone: Phone): Money = {
val fee = super.calculateFee(phone)
return fee + fee * taxRate
}
}
trait RateDiscountablePolicy extends BasicRatePolicy {
val discountAmount: Money
override def calcualteFee(phone: Phone): Money = {
val fee = super.calculateFee(phone)
fee - discountAmount
}
}
BasicRatePolicy를 확장하고 있다.BasicRatePolicy나 그의 자손에 해당하는 경우에만 믹스인 가능함⭕️을 의미한다.TaxablePolicy 트레이트를 사용하는 개발자의 실수를 막을 수 있다.extends 코드는 단순히 TaxablePolicy가 사용될 수 있는 문맥을 제한할 뿐이다.super 참조가 가리키는 대상 역시 컴파일 시점이 아닌 실행 시점에 결정된다.super 참조는 동적으로 결정된다.extends로 상속받기with를 이용해 믹스인class TaxableRegularPolicy {
amount: Money,
seconds: Duration,
val taxRate: Double)
extends RegularPolicy(amount, seconds)
with TaxablePolicy
TaxableRegularPolicy > TaxablePolicy > RegularPolicy > BasicRatePolicy 순서로 진행된다.인스턴스가 오직 한 곳에서만 필요한 경우 사용가능하다.
코드 여러 곳에서 동일한 트레이트를 믹스인해서 사용해야 한다면 명시적으로 클래스를 정의하는 것이 좋다.
new RegularPolicy(Money(100), Duration.ofSeconds(10))
with RateDiscountablePolicy
with TaxablePolicy {
val discountAmount = Money(100)
val taxRate = 0.02
}