상속
- 부모 클래스와 자식 클래스 사이의 의존성은 컴파일타임에 해결
- is-a 관계
- 클래스 사이의 정적 관계
합성
- 부모 클래스와 자식 클래스 사이의 의존성은 런타임에 해결
- has-a 관계
- 객체 사이의 동적 관계
코드 재사용을 위해서는 객체 합성이 클래스 상속보다 더 좋은 방법이다.
📖 11.1 상속을 합성으로 변경하기
상속을 남용할 때 직면하는 세 가지 문제
- 불필요한 인터페이스 상속 문제
- 메서드 오버라이딩의 오작용 문제
- 부모 클래스와 자식 클래스의 동시 수정 문제
🔖 11.1.1 불필요한 인터페이스 상속 문제: java.util.Properties와 java.util.Stack
public class Properties {
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의 오퍼레이션들이 퍼블릭 인터페이스를 오염시키지 않는다. Stack또한Vector의 인스턴스 변수를Stack클래스의 인스턴스 변수로 선언함으로써 합성 관계로 변경할 수 있다.
🔖 11.1.2 메서드 오버라이딩의 오작용 문제: InstrumentedHashSet
@RequiredArgsConstructor
public class InstrumentedHashSet<E> implements Set<E> {
private static final int addCount = 0;
private final Set<E> set;
... // Overriding
}
HashSet에 대한 구현 결합도는 제거하면서 퍼블릭 인터페이스는 그대로 상속
포워딩
Set의 오버레이션을 오버라이딩한 인스턴스 메서드에서 내부의HashSet인스턴스에게 동일한 메서드 호출을 그대로 전달- 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드라고 부른다.
- 기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 없이 일부 작동 방식을 변경하고 싶은 경우에 유용한 기법
🔖 11.1.3 부모 클래스와 자식 클래스의 동시 수정 문제: PersonalPlaylist
public class PersonalPlaylist {
private Playlist playlist = new Playlist();
public void append(Song song) {
playlist.append(song);
}
public void remove(Song song) {
playlist.getTracks().remove(song);
playlist.getSingers().remove(song.getSinger());
}
}
- 향후에
Playlist의 내부 구현을 변경하더라도 파급효과를 최대한PersonalPlaylist내부로 캡슐화할 수 있다.
몽키 패치
- 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것
📖 11.2 상속으로 인한 조합의 폭발적인 증가
- 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정해야 한다.
- 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있다.
🔖 11.2.1 기본 정책과 부가 정책 조합하기
기본 정책
- 가입자의 통화 정보 기반
- 일반 요금제, 심야 할인 요금제
부가 정책
- 통화량과 무관하게 기본 정책에 선택적으로 추가할 수 있는 요금 방식
- 세금 정책, 기본 요금 할인 정책
- 기본 정책의 계산 결과에 적용
- 선택적으로 적용 가능
- 조합 가능
- 임의의 순서로 적용 가능
🔖 11.2.2 상속을 이용해서 기본 정책 구현하기
@RequiredArgsConstructor
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 abstract Money calculateCallFee(Call call);
}
@RequiredArgsConstructor
public class RegularPhone extends Phone {
private final Money amount;
private final Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@RequiredArgsConstructor
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private final Money nightlyAmount;
private final Money regularAmount;
private final Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
- 기본 정책으로만 요금 계산
🔖 11.2.3 기본 정책에 세금 정책 조합하기
public class TaxableRegularPhone extends RegularPhone {
private final 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));
}
}
- 결합도가 높아진다.
@RequiredArgsConstructor
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);
}
@RequiredArgsConstructor
public class RegularPhone extends Phone {
private final Money amount;
private final Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
@RequiredArgsConstructor
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private final Money nightlyAmount;
private final Money regularAmount;
private final Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
- 부모 클래스에 추상 메서드를 추가하면 꽤나 번거로워진다.
@RequiredArgsConstructor
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 Money afterCalculated(Money fee) {
return fee;
}
protected abstract Money calculateCallFee(Call call);
}
- 기본 구현을 함께 제공하면 오버라이딩할 필요가 없다.
- 훅 메서드(hook method)
- 추상 메서드와 동일하게 자식 클래스에서 오버라이딩할 의도로 메서드를 추가했지만 편의를 위해 기본 구현을 제공하는 메서드
public class TaxableRegularPhone extends RegularPhone {
private final 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));
}
}
public class TaxableNightlyDiscountPhone extends NightlyDiscountPhone {
private final 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));
}
}
- 부모 클래스의 이름을 제외하면 대부분의 코드가 거의 동일
🔖 11.2.4 기본 정책에 기본 요금 할인 정책 조합하기
public class RateDiscountableRegularPhone extends RegularPhone {
private final 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);
}
}
public class RateDiscountableNightlyDiscountPhone extends NightlyDiscountPhone {
private final 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);
}
}
- 또, 중복 코드가 추가되었다.
🔖 11.2.5 중복 코드의 덫에 걸리다
상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다.
public class TaxableAndRateDiscountableRegularPhone extends TaxableRegularPhone {
private final Money discountAmount;
public TaxableAndRateDiscountableRegularPhone(Money amount, Duration seconds, double taxRate, Money discountAmount) {
super(amount, seconds, taxRate);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).minus(discountAmount);
}
}
public class RateDiscountableAndTaxableRegularPhone extends RateDiscountableRegularPhone {
private final double taxRate;
public RateDiscountableAndTaxableRegularPhone(Money amount, Duration seconds, Money discountAmount, double taxRate) {
super(amount, seconds, discountAmount);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).plus(fee.times(taxRate));
}
}
public class TaxableAndDiscountableNightlyDiscountPhone extends TaxableNightlyDiscountPhone {
private final Money discountAmount;
public TaxableAndDiscountableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate, Money discountAmount) {
super(nightlyAmount, regularAmount, seconds, taxRate);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).minus(discountAmount);
}
}
public class RateDiscountableAndTaxableNightlyDiscountPhone extends RateDiscountableNightlyDiscountPhone {
private final double taxRate;
public RateDiscountableAndTaxableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, Money discountAmount, double taxRate) {
super(nightlyAmount, regularAmount, seconds, discountAmount);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).plus(fee.times(taxRate));
}
}
- 클래스 폭발(class explosion) 또는 조합의 폭발(combinational explosion)
- 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생
- 기능을 추가할 때뿐만 아니라 기능을 수정할 때도 문제가 됨.
📖 11.3 합성 관계로 변경하기
합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임에 객체의 관계를 변경할 수 있다.
🔖 11.3.1 기본 정책 합성하기
public interface RatePolicy {
Money calculateFee(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);
}
@RequiredArgsConstructor
public class RegularPolicy extends BasicRatePolicy {
private final Money amount;
private final Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@RequiredArgsConstructor
public class NightlyDiscountPolicy extends BasicRatePolicy {
private static final int LATE_NIGHT_HOUR = 22;
private final Money nightlyAmount;
private final Money regularAmount;
private final Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@RequiredArgsConstructor
public class Phone {
private final RatePolicy ratePolicy;
@Getter
private List<Call> calls = new ArrayList<>();
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
- 다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우에는 합성하는 객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에 필요한 객체를 설정할 수 있도록 구현하는 것이 일반적이다.
🔖 11.3.2 부가 정책 적용하기
@RequiredArgsConstructor
public abstract class AdditionalRatePolicy implements RatePolicy {
private final RatePolicy next;
@Override
public Money calculateFee(Phone phone) {
Money fee = next.calculateFee(phone);
return afterCalculated(fee);
}
protected abstract Money afterCalculated(Money fee);
}
public class TaxablePolicy extends AdditionalRatePolicy {
private final double taxRatio;
public TaxablePolicy(RatePolicy next, double taxRatio) {
super(next);
this.taxRatio = taxRatio;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRatio));
}
}
public class RateDiscountablePolicy extends AdditionalRatePolicy {
private final Money discountAmount;
public RateDiscountablePolicy(RatePolicy next, Money discountAmount) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
🔖 11.3.3 기본 정책과 부가 정책 합성하기
Phone phone = new Phone(new taxablePolicy(0.05, new RegularPolicy(...)))
- 객체를 조합하고 사용하는 방식이 상속을 사용한 방식보다 더 예측 가능하고 일관성 있다.
🔖 11.3.4 새로운 정책 추가하기
- 오직 하나의 클래스만 추가하고 런타임에 필요한 정책들을 조합해서 원하는 기능을 얻을 수 있다.
- 요구사항을 변경할 때 오직 하나의 클래스만 수정해도 된다.
- 변경 후의 설계는 단일 책임 원칙을 준수하고 있다.
🔖 11.3.5 객체 합성이 클래스 상속보다 더 좋은 방법이다
- 상속은 부모 클래스의 세부적인 구현에 자식 클래스를 강하게 결합시키기 때문에 코드의 진화를 방해한다.
- 상속이 구현을 재사용하는 데 비해 합성은 객체의 인터페이스를 재사용한다.
- 상속의 종류: 구현 상속 / 인터페이스 상속
- 이번 장에서 살펴본 상속의 단점은 구현 상속
📖 11.4 믹스인
- 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리키는 용어
- 코드를 다른 코드 안에 섞어 넣기 위한 방법
- 상속이 클래스와 클래스 사이의 관계를 고정시키는 데 비해 믹스인은 유연하게 관계를 재구성할 수 있다.
- 이펙티브 자바 아이템20
- java에서 인터페이스는 믹스인 정의에 안성맞춤이지만, 추상 클래스는 믹스인을 정의할 수 없다.
📖 11.4.1 쌓을 수 있는 변경
- 믹스인은 대상 클래스의 자식 클래스처럼 사용될 용도로 만들어지는 것
- 믹스인을 추상 서브클래스(abstract subclass)라고 부르기도 한다.
- 쌓을 수 있는 변경: 믹스인을 사용하면 특정한 클래스에 대한 변경 또는 확장을 독립적으로 구현한 후 필요한 시점에 차례대로 추가할 수 있다.
