diff --git a/README.md b/README.md
index e78c8a8..4dc9233 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,8 @@
* [4. 설계 품질과 트레이드오프]( #4-설계-품질과-트레이드-오프 )
* [5. 책임 할당하기]( #5-책임-할당하기 )
* [6. 메시지와 인터페이스]( #6-메시지와-인터페이스 )
-* [10.상속과 코드 재사용]( #10-상속과-코드-재사용 )
+* [10. 상속과 코드 재사용]( #10-상속과-코드-재사용 )
+* [11. 합성과 유연한 설계]( #11-합성과-유연한-설계 )
## ⦿ 소프트웨어 모듈이 가져야 하는 세 가지 기능
@@ -186,6 +187,31 @@
* 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것보다 자식 클래스의 추상적인 메서드를
부모 클래스로 올리는 것이 재사용성과 응집도 측면에서 더 뛰어난 결과를 얻을 수 있다.
-
-
+
+## 11. 합성과 유연한 설계
+ * [학습 코드](https://github.com/orchsik/study-object/pull/8)
+ * 합성의 이점
+ * [코드 재사용을 위해서는] 객체 합성이 클래스 상속보다 더 좋은 방법이다.
+ * 상속과 합성은 재사용의 대상이 다르다. 상속은 부모 클래스 안에 구현된 코드 자체를 재사용하지만 합성은 포함되는
+ 객체의 퍼블릭 인터페이스를 재사용한다. 따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한
+ 의존성으로 변경할 수 있다. 다시 말해서 클래스 사이의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있는 것 이다.
+ * 상속으로 인한 조합의 폭발적인 증가
+ * 부모 클래스의 메서드를 재사용하기 위해 super 호출을 사용하면 원하는 결과를 쉽게 얻을 수 있지만 자식 클래스와 부모 클래스 사이의 결합도가 높아지고 만다.
+ 결합도를 낮추는 방법은 자식 클래스가 부모 클래스의 메서드를 호출하지 않도록 부모 클래스에 추상 메서드를 제공하는 것이다.
+ * 훅 메서드(hook method)? 추상 메서드와 동일하게 자식 클래스에서 오버라이딩할 의도로 메서드를 추가했지만 편의를 위해 기본 구현을 제공하는 메서드
+ * 단일 상속만 지원하는 경우, 상속으로 인해 발생하는 중복 코드 문제를 해결하기가 쉽지 않다.
+ * 합성 관계로 변경
+ * 각 정책을 별도의 클래스로 구현하는 것이다. 분리된 정책들을 연결할 수 있도록 합성 관계를 이용해서 구조를 개선하면 실행 시점에 정책들들을 조합할 수 있게 된다.
+ * 다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우에는 합성하는 객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에
+ 필요한 객체를 생성할 수 있도록 구현하는 것이 일반적이다.
+ * 부가 정책은 기본 정책이나 다른 부가 정책의 인스턴스를 참조할 수 있어야 한다. 다시 말해서 부가 정책의 인스턴스는 어떤 종류의 정책과도 합성될 수 있어야 한다.
+ Phone의 입장에서는 자신이 기본 정책의 인스턴스에게 메시지를 전송하고 있는지, 부가 정책의 인스턴스에게 메시지를 전송하고 있는지를 몰라야 한다. 다시 말해서
+ 기본 정책과 부가 정책은 협력 안에서 동일한 `역할`을 수행해야 한다. 이것은 부가 정책이 기본 정책과 동일한 RatePolicy 인터페이스를 구현해야 한다는 것을 의미한다.
+ * 상속을 사용하면 안 되는 것인가? 상속을 사용해야 하는 경우는 언제인가?
+ * 상속은 구현 상속과 인터페이스 상속이 있다. 13장을 읽으면 구현 상속을 피하고 인터페이스 상속을 사용해야 하는 이유를 알 수 있다.
+ * 믹스인
+ * 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법이다.
+ * 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재새용 방법이다.
+ * 상속과 다르다. 믹스인은 말 그대로 코드를 다른 코드 안에 섞에 넣기 위한 방법이다. (상속의 목적은 자식 클래스를 부모 클래스와 동일한 개념적인 범주로 묶어 is-a 관계를 만드는 것)
+ * 상속이 클래스와 클래스 사이의 관계를 고정시키는 데 비해 믹스인은 유연하게 관계를 재구성할 수 있다.
diff --git a/app/src/main/java/com/orchsik/object/_11_call/Call.java b/app/src/main/java/com/orchsik/object/_11_call/Call.java
new file mode 100644
index 0000000..9fd66b7
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/Call.java
@@ -0,0 +1,23 @@
+package com.orchsik.object._11_call;
+
+import lombok.Getter;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * 개별 통화 시간.
+ */
+public class Call {
+ @Getter
+ private LocalDateTime from;
+ private LocalDateTime to;
+
+ public Call(LocalDateTime from, LocalDateTime to) {
+ this.from = from;
+ this.to = to;
+ }
+
+ public Duration getDuration() {
+ return Duration.between(from, to);
+ }
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/NightlyDiscountPhone.java b/app/src/main/java/com/orchsik/object/_11_call/NightlyDiscountPhone.java
new file mode 100644
index 0000000..053f2b0
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/NightlyDiscountPhone.java
@@ -0,0 +1,28 @@
+package com.orchsik.object._11_call;
+
+import java.time.Duration;
+
+import com.orchsik.object._02_movie.Money;
+
+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(call.getDuration().getSeconds() / seconds.getSeconds());
+ }
+ return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/Phone.java b/app/src/main/java/com/orchsik/object/_11_call/Phone.java
new file mode 100644
index 0000000..910edca
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/Phone.java
@@ -0,0 +1,24 @@
+package com.orchsik.object._11_call;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.orchsik.object._02_movie.Money;
+
+public abstract class Phone {
+ private List calls = new ArrayList<>();
+
+ public Money calculateFee() {
+ Money result = Money.ZERO;
+ for (Call call : calls) {
+ result = result.plus(calculateCallFee(call));
+ }
+ return result;
+ }
+
+ protected Money afterCalculated(Money fee) {
+ return fee;
+ };
+
+ protected abstract Money calculateCallFee(Call call);
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/RateDiscountableNightlyDiscountPhone.java b/app/src/main/java/com/orchsik/object/_11_call/RateDiscountableNightlyDiscountPhone.java
new file mode 100644
index 0000000..9b0e726
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/RateDiscountableNightlyDiscountPhone.java
@@ -0,0 +1,20 @@
+package com.orchsik.object._11_call;
+
+import java.time.Duration;
+
+import com.orchsik.object._02_movie.Money;
+
+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);
+ }
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/RateDiscountableRegularPhone.java b/app/src/main/java/com/orchsik/object/_11_call/RateDiscountableRegularPhone.java
new file mode 100644
index 0000000..68c2926
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/RateDiscountableRegularPhone.java
@@ -0,0 +1,19 @@
+package com.orchsik.object._11_call;
+
+import java.time.Duration;
+import com.orchsik.object._02_movie.Money;
+
+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);
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/RegularPhone.java b/app/src/main/java/com/orchsik/object/_11_call/RegularPhone.java
new file mode 100644
index 0000000..c7e1e06
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/RegularPhone.java
@@ -0,0 +1,20 @@
+package com.orchsik.object._11_call;
+
+import java.time.Duration;
+import com.orchsik.object._02_movie.Money;
+
+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(call.getDuration().getSeconds() / seconds.getSeconds());
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/TaxableNightlyDiscountPhone.java b/app/src/main/java/com/orchsik/object/_11_call/TaxableNightlyDiscountPhone.java
new file mode 100644
index 0000000..1f9a9fa
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/TaxableNightlyDiscountPhone.java
@@ -0,0 +1,19 @@
+package com.orchsik.object._11_call;
+
+import java.time.Duration;
+import com.orchsik.object._02_movie.Money;
+
+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));
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call/TaxableRegularPhone.java b/app/src/main/java/com/orchsik/object/_11_call/TaxableRegularPhone.java
new file mode 100644
index 0000000..6eb86d3
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call/TaxableRegularPhone.java
@@ -0,0 +1,19 @@
+package com.orchsik.object._11_call;
+
+import java.time.Duration;
+import com.orchsik.object._02_movie.Money;
+
+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));
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/AdditionalRatePolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/AdditionalRatePolicy.java
new file mode 100644
index 0000000..d1f6965
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/AdditionalRatePolicy.java
@@ -0,0 +1,20 @@
+package com.orchsik.object._11_call_composition;
+
+import com.orchsik.object._02_movie.Money;
+
+public abstract class AdditionalRatePolicy implements RatePolicy {
+ private RatePolicy next;
+
+ public AdditionalRatePolicy(RatePolicy next) {
+ this.next = next;
+ }
+
+ @Override
+ public Money calculateFee(Phone phone) {
+ Money fee = next.calculateFee(phone);
+ return afterCalculate(fee);
+ }
+
+ protected abstract Money afterCalculate(Money fee);
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/BasicRatePolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/BasicRatePolicy.java
new file mode 100644
index 0000000..70f75fc
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/BasicRatePolicy.java
@@ -0,0 +1,19 @@
+package com.orchsik.object._11_call_composition;
+
+import com.orchsik.object._02_movie.Money;
+
+// 기본 정책을 구성하는 일반 요금제와 심야 할인 요금제는 개별 요금을 계산하는 방식을 제외한 전체 처리 로직이 거의 동일하다.
+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 null;
+ }
+
+ protected abstract Money calculateCallFee(Call call);
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/Call.java b/app/src/main/java/com/orchsik/object/_11_call_composition/Call.java
new file mode 100644
index 0000000..c34027a
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/Call.java
@@ -0,0 +1,23 @@
+package com.orchsik.object._11_call_composition;
+
+import lombok.Getter;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * 개별 통화 시간.
+ */
+public class Call {
+ @Getter
+ private LocalDateTime from;
+ private LocalDateTime to;
+
+ public Call(LocalDateTime from, LocalDateTime to) {
+ this.from = from;
+ this.to = to;
+ }
+
+ public Duration getDuration() {
+ return Duration.between(from, to);
+ }
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/NightlyDiscountPolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/NightlyDiscountPolicy.java
new file mode 100644
index 0000000..f516ec7
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/NightlyDiscountPolicy.java
@@ -0,0 +1,27 @@
+package com.orchsik.object._11_call_composition;
+
+import java.time.Duration;
+import com.orchsik.object._02_movie.Money;
+
+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());
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/Phone.java b/app/src/main/java/com/orchsik/object/_11_call_composition/Phone.java
new file mode 100644
index 0000000..eb20348
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/Phone.java
@@ -0,0 +1,24 @@
+package com.orchsik.object._11_call_composition;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import com.orchsik.object._02_movie.Money;
+
+@Getter()
+public abstract class Phone {
+ // Phone 내부에 RatePolicy에 대한 참조자가 포함돼 있다는 것에 주목하라. 이것이 바로 합성이다.
+ // Phone이 다양한 요금 정채과 협력할 수 있어야 하므로 요금 정책의 타입이 RatePolicy라는 인터페이스로 정의돼 있다는 것에도
+ // 주목하라.
+ private RatePolicy ratePolicy;
+ private List calls = new ArrayList<>();
+
+ public Phone(RatePolicy ratePolicy) {
+ this.ratePolicy = ratePolicy;
+ }
+
+ public Money calculateFee() {
+ return ratePolicy.calculateFee(this);
+ };
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/RateDiscountablePolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/RateDiscountablePolicy.java
new file mode 100644
index 0000000..f53eaba
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/RateDiscountablePolicy.java
@@ -0,0 +1,18 @@
+package com.orchsik.object._11_call_composition;
+
+import com.orchsik.object._02_movie.Money;
+
+public class RateDiscountablePolicy extends AdditionalRatePolicy {
+ private Money discountAmount;
+
+ public RateDiscountablePolicy(Money discountAmount, RatePolicy next) {
+ super(next);
+ this.discountAmount = discountAmount;
+ }
+
+ @Override
+ protected Money afterCalculate(Money fee) {
+ return fee.minus(discountAmount);
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/RatePolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/RatePolicy.java
new file mode 100644
index 0000000..1252d3a
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/RatePolicy.java
@@ -0,0 +1,7 @@
+package com.orchsik.object._11_call_composition;
+
+import com.orchsik.object._02_movie.Money;
+
+public interface RatePolicy {
+ Money calculateFee(Phone phone);
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/RegularPolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/RegularPolicy.java
new file mode 100644
index 0000000..a1c5a0d
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/RegularPolicy.java
@@ -0,0 +1,20 @@
+package com.orchsik.object._11_call_composition;
+
+import java.time.Duration;
+import com.orchsik.object._02_movie.Money;
+
+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());
+ }
+
+}
diff --git a/app/src/main/java/com/orchsik/object/_11_call_composition/TaxablePolicy.java b/app/src/main/java/com/orchsik/object/_11_call_composition/TaxablePolicy.java
new file mode 100644
index 0000000..0676af3
--- /dev/null
+++ b/app/src/main/java/com/orchsik/object/_11_call_composition/TaxablePolicy.java
@@ -0,0 +1,18 @@
+package com.orchsik.object._11_call_composition;
+
+import com.orchsik.object._02_movie.Money;
+
+public class TaxablePolicy extends AdditionalRatePolicy {
+ private double taxRatio;
+
+ public TaxablePolicy(RatePolicy next, double taxRatio) {
+ super(next);
+ this.taxRatio = taxRatio;
+ }
+
+ @Override
+ protected Money afterCalculate(Money fee) {
+ return fee.plus(fee.times(taxRatio));
+ }
+
+}