상속은 클라스 사이의 정적인 관계인 것에 비해, 합성은 객체 사이의 동적인 관계다.
따라서, 상속은 변경이 불가능하지만, 합성은 실행 시점에 동적으로 변경할 수 있다.
사용의 용이함을 가진 상속보다, 변경에 유연한 대처를 가진 합성을 선택해라.
[코드 재사용을 위해서는] 객체 합성이 클래스의 상속보다 더 좋은 방법이다. [GOF94].
객체지향 시스템에서 기능을 재사용할 수 있는 가장 대표적인 기법은 클래스 상속(class inheritance)과 객체 합성(object composition)이다. ... 클래스 상속은 다른 클래스를 이용해서 한 클래스의 구현을 정의하는 것이다. 서브 클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)이라고 부른다. 화이트박스라는 말은 가시성때문에 나온 말이다. 상속을 받으면 부모 클래스의 내부가 자식 클래스에 공개되기 때문에 화이트박스인 셈이다.
객체 합성은 클래스 상속의 대안이다. 새로운 기능을 위해 객체들을 합성한다. 객체를 합성하려면 합성할 객체들의 인터페이스를 명확하게 정의해야만 한다. 이런 스타일의 재사용을 블랙박스 재사용(balck-box reuse)이라고 하는데, 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되기 때문이다.[GOF67].
java.util.Properties
, java.util.Stack
java.util.HashSet
을 상속받은 InstrumentHashSet
Playlist
를 상속받은 PersonalPlaylist
합성을 사용하면 상속이 초래한 문제점들을 해결할 수 있다.
상속을 합성으로 변경해 문제를 해결해보자!
상속을 합성으로 변경하는 방법
1. 자식 클래스에 선언된 상속 관계를 제거
2. 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 변수로 선언
java.util.Properties
public 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.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);
}
}
Stack
의 퍼블릭 인터페이스에 불필요한 Vector
의 오퍼레이션이 포함되지 않는다.Vector
를 통해 Stack
의 규칙을 어기고 잘못 사용할 수 있을 가능성을 깔끔하게 제거했다.InstrumentedHashSet
InstrumentedHashSet
은 앞서 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
}