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)); + } + +}