Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* [4. 설계 품질과 트레이드오프]( #4-설계-품질과-트레이드-오프 )
* [5. 책임 할당하기]( #5-책임-할당하기 )
* [6. 메시지와 인터페이스]( #6-메시지와-인터페이스 )
* [10.상속과 코드 재사용]( #10-상속과-코드-재사용 )
* [10. 상속과 코드 재사용]( #10-상속과-코드-재사용 )
* [11. 합성과 유연한 설계]( #11-합성과-유연한-설계 )


## ⦿ 소프트웨어 모듈이 가져야 하는 세 가지 기능
Expand Down Expand Up @@ -186,6 +187,31 @@
* 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것보다 자식 클래스의 추상적인 메서드를
부모 클래스로 올리는 것이 재사용성과 응집도 측면에서 더 뛰어난 결과를 얻을 수 있다.



<br/>

## 11. 합성과 유연한 설계
* [학습 코드](https://github.com/orchsik/study-object/pull/8)
* 합성의 이점
* [코드 재사용을 위해서는] 객체 합성이 클래스 상속보다 더 좋은 방법이다.
* 상속과 합성은 재사용의 대상이 다르다. 상속은 부모 클래스 안에 구현된 코드 자체를 재사용하지만 합성은 포함되는
객체의 퍼블릭 인터페이스를 재사용한다. 따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한
의존성으로 변경할 수 있다. 다시 말해서 클래스 사이의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있는 것 이다.
* 상속으로 인한 조합의 폭발적인 증가
* 부모 클래스의 메서드를 재사용하기 위해 super 호출을 사용하면 원하는 결과를 쉽게 얻을 수 있지만 자식 클래스와 부모 클래스 사이의 결합도가 높아지고 만다.
결합도를 낮추는 방법은 자식 클래스가 부모 클래스의 메서드를 호출하지 않도록 부모 클래스에 추상 메서드를 제공하는 것이다.
* 훅 메서드(hook method)? 추상 메서드와 동일하게 자식 클래스에서 오버라이딩할 의도로 메서드를 추가했지만 편의를 위해 기본 구현을 제공하는 메서드
* 단일 상속만 지원하는 경우, 상속으로 인해 발생하는 중복 코드 문제를 해결하기가 쉽지 않다.
* 합성 관계로 변경
* 각 정책을 별도의 클래스로 구현하는 것이다. 분리된 정책들을 연결할 수 있도록 합성 관계를 이용해서 구조를 개선하면 실행 시점에 정책들들을 조합할 수 있게 된다.
* 다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우에는 합성하는 객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에
필요한 객체를 생성할 수 있도록 구현하는 것이 일반적이다.
* 부가 정책은 기본 정책이나 다른 부가 정책의 인스턴스를 참조할 수 있어야 한다. 다시 말해서 부가 정책의 인스턴스는 어떤 종류의 정책과도 합성될 수 있어야 한다.
Phone의 입장에서는 자신이 기본 정책의 인스턴스에게 메시지를 전송하고 있는지, 부가 정책의 인스턴스에게 메시지를 전송하고 있는지를 몰라야 한다. 다시 말해서
기본 정책과 부가 정책은 협력 안에서 동일한 `역할`을 수행해야 한다. 이것은 부가 정책이 기본 정책과 동일한 RatePolicy 인터페이스를 구현해야 한다는 것을 의미한다.
* 상속을 사용하면 안 되는 것인가? 상속을 사용해야 하는 경우는 언제인가?
* 상속은 구현 상속과 인터페이스 상속이 있다. 13장을 읽으면 구현 상속을 피하고 인터페이스 상속을 사용해야 하는 이유를 알 수 있다.
* 믹스인
* 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법이다.
* 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재새용 방법이다.
* 상속과 다르다. 믹스인은 말 그대로 코드를 다른 코드 안에 섞에 넣기 위한 방법이다. (상속의 목적은 자식 클래스를 부모 클래스와 동일한 개념적인 범주로 묶어 is-a 관계를 만드는 것)
* 상속이 클래스와 클래스 사이의 관계를 고정시키는 데 비해 믹스인은 유연하게 관계를 재구성할 수 있다.
23 changes: 23 additions & 0 deletions app/src/main/java/com/orchsik/object/_11_call/Call.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}

}
24 changes: 24 additions & 0 deletions app/src/main/java/com/orchsik/object/_11_call/Phone.java
Original file line number Diff line number Diff line change
@@ -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<Call> 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);

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}

}
20 changes: 20 additions & 0 deletions app/src/main/java/com/orchsik/object/_11_call/RegularPhone.java
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
@@ -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<Call> calls = new ArrayList<>();

public Phone(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}

public Money calculateFee() {
return ratePolicy.calculateFee(this);
};

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Loading