반응형
오브젝트 11 - 합성과 유연한 설계
- 상속은 부모 클래스와 자식 클래스를 연결해서 부모 클래스의 코드를 재사용한다.
- 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.
- 상속에서의 의존성은 컴파일타임에 해결된다.
- 합성에서의 의존성은 런타임에 해결된다.
11.1 상속을 합성으로 변경하기
- 코드 재사용을 위해 상속을 남용하면 아래와 같은 세가지 문제점에 직면한다.
- 불필요한 인터페이스 상속 문제 - 자식 클래스에게는 부적합한 부모 클래스의 오퍼레이션이 상속된다.
- 메서드 오버라이딩의 오작용 문제 - 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는다.
- 부모 클래스와 자식 클래스의 동시 수정 문제 - 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 하는 문제
- 합성을 사용하면 상속이 초래하는 세가지 문제점을 해결 할 수 있다.
- 상속 관계를 제거하고 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 변수로 선언하면 된다.
11.1.1 불필요한 인터페이스 상속 문제 해결
- Hashtable 클래스와 Properties 클래스를 합성 관계로 바꿔보자
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의 오퍼레이션들이 Properties 클래스의 퍼블릭 인터페이스를 오염시키지 않는다.
- Vetor를 상속받는 Stack 역시 Vector의 인스턴스 변수를 Stack 클래스의 인스턴스 변수로 선언함으로써 합성 할 수 있다.
public class Stack<E> {
private Vector<E> elements = new Vector<>();
public Epush(Eitem) {
elements.addElement(item);
return item;
}
public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
}
11.1.2 메서드 오버라이딩의 오작용 문제 해결
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;
}
}
11.1.3 부모 클래스와 자식 클래스의 동시 수정 문제
- Playlist는 합성으로 변경하더라도 가수별 노래 목록을 유지하기 위해 Playlist와 PersonalPlaylist를 함께 수정해야하는 문제는 해결되지 않는다.
- 그럼에도 여전히 상속보다는 합성을 사용하는 것이 좋다.
11.2 상속으로 인한 조합의 폭발적인 증가
- 상속으로 인해 결합도가 높아지면 코드를 수정하는데 필요한 작업의 양이 과도하게 늘어나는 경향이 있다.
- 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정해야 한다.
- 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있다.
- 합성을 사용하면 상속으로 인해 발생하는 클래스 증가와 중복 코드 문제를 간단하게 해결할 수 있다.
11.2.1 기본 정책과 부가 정책 조합하기
- 핸드폰 요금제가 '기본 정책'과 '부가 정책'을 조합해서 구성된다고 가정하자
- 현재의 기본 정책과 부가 정책을 조합해서 만들 수 있는 모든 요금 정책의 종류는 아래와 같다.
- 설계는 다양한 조합을 수용할 수 있도록 유연해야 한다.
11.2.2 상속을 이용해서 기본 정책 구현하기
- 기본 정책은 Phone 추상 클래스를 루트로 삼고, 일반 요금제 RegularPhone과 심야 할인 요금제 NightlyDiscountPhone은 Phone의 자식 클래스로 구현한다.
- 부가 정책은 적용하지 않고 오직 기본 정책만 적용한다.
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;
}
abstract protected Money calculateCallFee(Call call);
}
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());
}
}
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());
}
}
11.2.3 기본 정책에 세금 정책 조합하기
- 일반 요금제에 세금 정책을 조합하는 가장 간단한 방법은 RegularPhone 클래스를 상속받은 TaxableRegularPhone 클래스를 추가하는 것이다.
- 심야 할인 요금제인 NightlyDiscountPhone 에도 세금을 부과할 수 있게 TaxableNightlyDiscountPhone 자식 클래스를 추가하자
11.2.4 기본 정책에 기본 요금 할인 정책 조합하기
- 이번에는 두번째 부가 정책인 기본 요금 할인 정책을 적용해보자.
- 일반 요금제에 기본 요금 할인 정책을 조합하는 가장 간단한 방법은 RegularPhone 클래스를 상속받은 RateDiscountableRegularPhone 클래스를 추가하는 것이다.
- 심야 할인 요금제인 NightlyDiscountPhone 에도 세금을 부과할 수 있게 RateDiscountableNightlyDiscountPhone 자식 클래스를 추가하자
11.2.5 중복 코드의 덫에 걸리다
- 부가 정책은 자유롭게 조합할 수 있어야 하고 순서 역시 임의로 결정할 수 있어야 한다.
- 상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다.
- 위의 상속 계층에 새로운 기본 정책을 추가해야 한다고 가정해보자.
- 추가할 기본 정책은 '고정 요금제'로 구현할 것이다.
- 여기에 새로운 부가 정책을 추가하는 경우를 생각해보자...그만 생각하자
- 이처럼 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 클래스 폭발 문제 또는 조합의 폭발 문제라고 부른다.
- 클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계때문에 발생하는 문제다.
- 클래스 폭발 문제는 새로운 기능을 추가할 때뿐만 아니라 기능을 수정할 때도 문제가 된다.
- 중복된 코드를 모두 찾아 동일한 방식으로 수정해야 한다.
11.3 합성 관계로 변경하기
- 컴파일시에 결정되고 고정되는 상속 관계를 런타임 관계로 변경함으로써 이 문제를 해결한다.
- 합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임 객체의 관계를 변경할 수 있다.
- 상속을 합성으로 변경해보자
11.3.1 기본 정책 합성하기
- 가장 먼저 할일은 각 정책을 별도의 클래스로 구현하는 것이다.
- 먼저 기본 정책과 부과 정책을 포괄하는 RatePolicy 인터페이스를 추가하고 기본 정책을 구현하자.
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);
}
- BasicRatePolicy를 상속받아 일반 요금제와 심야 할인 요금제를 구현하자.
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.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
- 이제 기본 정책을 이용해 요금을 계산할 수 있도록 Phone을 수정하자
public class Phone {
private RatePolicy ratePolicy;
private List<Call> calls = new ArrayList<>();
public List<Call> getCalls() {
return Collections.unmodifiablelist(calls);
}
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
- Phone의 내부에 RatePolicy가 있는것이 바로 합성이다.
- Phone은 다양한 요금 정책과 협력할 수 있어야 하므로 요금 정책의 타입이 RatePolicy라는 인터페이스로 정의되어있다.
- 합성은 합성하는 객체의 타입을 인터페이스나 추상클래스로 선언하고 의존성 주입을 통해 런타임에 필요한 객체를 주입한다.
11.3.2 부가 정책 적용하기
- 부가 정책은 기본 정책이나 다른 부가 정책의 인스턴스를 참조할 수 있어야 한다.
- 기본 정책과 부가 정책은 협력 안에서 동일한 '역할'을 수행해야 한다.
- 즉, 부가 정책은 RatePolicy 인터페이스를 구현해야 하며, 내부에 또 다른 RatePolicy 인스턴스를 합성할 수 있어야 한다.
- 부가 정책을 AdditionalRatePolicy 추상 클래스로 구현하자
public abstract class AdditionalRatePolicy implements RatePolicy {
private RatePolicy next;
@Override
public Money calculateFee(Phone phone) {
Money fee = next.calculateFee(phone);
return afterCalculated(fee);
}
abstract protected Money afterCalculated(Money fee);
}
- 그 후, 세금 정책과 기본 요금 할인 정책을 구현하자
public class TaxablePolicy extends AdditionalRatePolicy {
private double taxRatio;
public TaxablePolicy(double taxRatio, RatePolicy next) {
super(next);
this.taxRatio = taxRatio;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRatio));
}
}
public class RateDiscountablePolicy extends AdditionalRatePolicy {
private Money discountAmount;
public RateDiscountablePolicy(Money discountAmount, RatePolicy next) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
11.3.3 기본 정책과 부가 정책 합성하기
// 1. 일반 요금제에 세금 정책을 조합할 경우
Phone phone = new Phone(new TaxablePolicy(0.05,new RegularPolicy(...));
// 2. 일반 요금제에 기본 요금 할인 정책 조합 후 세금 정책을 조합할 경우
Phone phone = new Phone(new TaxablePolicy(0.05,new RateDiscountablePolicy(Money.wons(1000),new RegularPolicy(...)));
// 3. 일반 요금제에 세금 정책 조합 후 기본 요금 할인 정책을 조합할 경우
Phone phone = new Phone(new RateDiscountablePolicy(Money.wons(1000),new TaxablePolicy(0.05,new RegularPolicy(...)));
// 4. 심야 할인 요금제에 세금 정책 조합 후 기본 요금 할인 정책을 조합할 경우
Phone phone = new Phone(new RateDiscountablePolicy(Money.wons(1000),new TaxablePolicy(0.05,new NightDiscountPolicy(...)));
- 객체를 조합하고 사용하는 방식이 상속을 사용한 방식보다 더 예측 가능하고 일관성 있다.
- 합성의 진가는 새로운 클래스를 추가하거나 수정하는 시점이 되어서야 진가가 나타난다.
11.3.4 새로운 정책 추가하기
- 상속과 달리 고정 요금제가 필요하다면 고정 요금제를 구현한 클래스 '하나'만 추가한 후 원하는 대로 조합하면 된다.
- 새로운 부가 정책 역시 클래스 '하나'만 추가하면 된다.
11.3.5 객체 합성이 클래스 상속보다 더 좋은 방법이다.
- 상속은 부모의 클래스의 세부적인 구현에 자식 클래스가 강하게 결합되어 코드의 진화를 방해한다.
- 코드를 재사용하면서도 건전한 결합도를 유지할 수 있는 더 좋은 방법은 합성을 이용하는 것이다.
- 그렇다면 상속은 사용해서는 안되는것인가?
- 지금까지 살펴본 상속에 대한 모든 단점들은 구현 상속에 국한된다.
- 구현 상속을 피하고 인터페이스 상속을 사용해야 한다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 13 - 서브클래싱과 서브타이핑 (0) | 2024.04.09 |
---|---|
오브젝트 12 - 다형성 (0) | 2024.04.02 |
오브젝트 10 - 상속과 코드 재사용 (0) | 2024.03.26 |
오브젝트 09 - 유연한 설계 (0) | 2024.03.25 |
오브젝트 07 - 객체 분해 (1) | 2024.03.05 |
댓글