11. 합성과 유연한 설계
태그
비어 있음
날짜
2024년 5월 5일
서브클래싱에 의한 재사용을 화이트박스 재사용
합성할 객체들의 인터페이스에 의한 재사용을 블랙박스 재사용이라고 한다.
🏁 상속을 합성으로 변경하기
10장 상속에서
불필요한 인터페이스 상속 문제 예시에서 봤던 Stack코드를 합성을 사용하면 이렇게 변경할 수 있다.
Java
복사
public class Stack<E> {
private Vector<E> elements = new Vector<>();
public E push(E item) {
elements.add(item);
return item;
}
public E pop() {
if(elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size()-1);
}
}
메서드 오버라이딩의 오작용 문제에서 봤던 HashSet을 상속받은 InstrumentedHashSet은 이렇게 변경할 수 있다.
HashSet이 제공하는 인터페이스도 제공해야 하므로 Set 인터페이스를 실체화 하면서 HashSet인스턴스를 합성함.
Java
복사
public class InstrumentedHashSet<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
addCount++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
@Override public boolean remove(E e) {return set.remove(e);}
@Override public void clear() {return set.clear();}
@Override public boolean equals(E e) {return set.equals(e);}
@Override public int hashCode() {return set.hashCode();}
@Override public Spliterator<E> spliterator() {return set.spliterator();}
@Override public boolean isEmpty() {return set.isEmpty(e);}
@Override public boolean contains(E e) {return set.contains(e);}
@Override public Iterator<E> iterator() {return set.iterator();}
@Override public E[] toArray(E e) {return set.remove(e);}
}
🏁 상속으로 인한 조합의 폭발적인 증가
기본정책에 부가 정책을 조합할 때
기본 정책의 계산 결과에 적용되어야 하고
선택적으로 적용할 수 있어야 하고
조합 가능해야 하고
부가 정책은 임의의 순서로 적용 가능해야 한다.
따라서 조합 가능한 모든 순서는 다음과 같다.
상속을 이용하면 상속 계층은 그림처럼 복잡해지는데 더 큰 문제는 새로운 정책을 추가하기 어렵다는 것이다.
다음 그림은 고정요금제를 추가한 것이다.
여기다가 다른 부가 정책과의 조합도 가능하게 만들면 정말 많은 클래스가 추가되어야 한다.
또한 하나를 수정할 때 같이 수정해야 하는 클래스들도 많아진다.
💡
상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가 해야 하는 경우를 가리켜 클래스 폭발 문제 또는 조합의 폭발 문제하고 부른다.
이 문제를 해결할 수 있는 최선의 방법은 상속을 포기하는 것이다.
🏁 합성 관계로 변경하기
합성은 조합을 구성하는 요소들을 개별 클래스로 구현한 후 실행 시점에 인스턴스를 조립하는 방법을 사용하는 것
인터페이스
Java
복사
public interface RatePolicy {
Money calculateFee(Phone phone);
}
중복 코드를 담을 추상 클래스
Java
복사
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);
}
calculateCallFee를 구현해 요금을 계산하는 클래스
Java
복사
public class RegularPolicy extends BasicRatePolicy {
private Money amount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
public class NightlyDiscountPolicy extends BasicRatePolicy {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getStartTime().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
public class Phone {
private RatePolicy ratePolicy; // 추상화에 의존
private List<Call> calls = new ArrayList<>();
public Phone(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
부가 정책 적용하기
다른 기본 정책이나 부가 정책 인스턴스를 참조하고 어떤 종류의 정책과도 합성될 수 있어야 한다.
기본정책과 부가정책은 협력 안에서 동일한 역할을 수행해야 하므로 RatePolicy 인터페이스를 구현해야 한다.
추상 클래스
Java
복사
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 afterCalculated(fee);
}
abstract protected Money afterCalculated(Money fee);
}
세금 정책과 기본 요금 할인 정책 구현
Java
복사
public class TaxablePolicy extends AdditionalRatePolicy {
private double taxRate;
public TaxablePolicy(double taxRate, RatePolicy next) {
super(next);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRate));
}
}
Java
복사
public class RateDiscountPolicy extends AdditionalRatePolicy {
private Money discountAmount;
public RateDiscountPolicy(Money discountAmount, RatePolicy next) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
모든 요금 계산과 관련된 모든 클래스 사이의 관계를 나타내는 다이어그램
새로운 정책을 추가할 떄도 클래스 ‘하나’만 추가하면 된다.
🏁 믹스인
합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다.
상속은 정적이지만 믹스인은 동적이다.
super 참조가 가리키는 대상이 컴파일 시점이 아닌 실행 시점에 결정된다
상속은 재사용 가능한 문맥을 고정시키지만 트레이트는 문맥을 확장 가능하도록 열어 놓는다.
extends는 상속이 아니라 BasicRatePolicy를 상속받은 것만 믹스인 될 수 있다는 뜻이다.
Scala
복사
trait RateDiscountablePolicy extends BasicRatePolicy {
val discountAmount: Money
override def calculateFee(phone: Phone): Money = {
val fee = super.calculateFee(phone)
fee - discountAmount
}
}
표준 요금제를 적용 → 비율 할인 정책 → 세금 정책 적용
자기 자신
오른쪽부터 트레이트 쌓아올림
즉 TaxablePolicy가 RateDiscountPolicy를 실행 한 뒤 실행됨.
즉 비율 할인 정책 먼저 적용.
Scala
복사
class RateDiscountableAndTaxableRegularPolicy(
amount: Money,
seconds: Duration,
val taxRate: Double)
extends RegularPolicy(amount, seconds)
with RateDiscountablePolicy
with TaxablePolicy
느낀점
상속의 안좋은 점을 이어 합성으로 구현하는 것과 리팩토링 하는 법을 배우니까 더 효과적인 것 같다.
클래스 폭발 예제가 합성의 안좋은 점을 이해시키기 좋았던 것 같다.
또한 몰랐던 언어(스칼라)의 믹스인이라는 것을 배울 수 있어서 좋았다.
동적언어나 스칼라에 대해서 전혀 몰랐는데 호기심이 생겼다.
다시 한번 역할의 중요성을 깨달은 것 같다.
표
'책 > 오브젝트' 카테고리의 다른 글
13. 서브클래싱과 서브타이핑 (0) | 2025.03.17 |
---|---|
12. 다형성 (0) | 2025.03.17 |
10. 상속과 코드 재사용 (0) | 2025.03.17 |
09. 유연한 설계 (0) | 2025.03.17 |
08. 의존성 관리하기 (0) | 2025.03.17 |