TIL

11장 합성과 유연한 설계

01 상속을 합성으로 변경하기

불필요한 인터페이스 상속 문제: 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);
    }
}

public class Stack<E> {
    private Vector<E> elements = new Vector<>();

    public E push(E item) {
        elements.addElement(item);
        return item;
    }

    public E pop() {
        if (elements.isEmpty()) {
            throw enw EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }
}

메서드 오버라이딩의 오작용 문제: InstrumentedHashSet

public class InstrumentedSet<E> {

	private int addCount = 0;
	private final Set<E> set;

	public InstrumentedSet(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(Object o) { return set.remote(o); }

@Override
public void clear() { set.clear(); }
// ...

부모 클래스와 자식 클래스의 동시 수정 문제: PersonalPlaylist

public class PersonalPlaylist {
    private Playlist playlist = new Playlist();

    public void append(Song song) {
        playlist.append(song);
    }

    public void remote(Song song) {
        playlist.getTracks().remote(song);
        playlist.getSingers().remote(song.getSinger());
    }
}

02 상속으로 인한 조합의 폭발적인 증가

기본 정책과 부가 정책 조합하기

상속을 이용해서 기본 정책 구현하기

기본 정책에 세금 정책 조합하기

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 Money afterCalculated(Money fee) {
        return fee;
    }
}
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));
    }
}

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

기본 정책에 기본 요금 할인 정책 조합하기

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

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

중복 코드의 덫에 걸리다.

03 합성 관계로 변경하기

기본 정책 합성하기

// 기본 정책과 부가 정책을 포괄하는 인터페이스
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);
}

// 일반 요금제
public class RegularPolicy extends BasicRatePolicy { ... }

// 심야 할인 요금제
public class NightlyDiscountPolicy extends BasicRatePolicy { ... }
public class Phone {
    private RatePolicy ratePolicy;
    private List<Call> calls = new ArrayList<>();

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

    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    }

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

부가 정책 적용하기

// 부가 정책
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);
}

// 세금 정책
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);
    }
}

기본 정책과 부가 정책 합성하기

// 일반 요금제에 세금 정책 조합
Phone phone = new Phone(
        new TaxablePolicy(0.05, new RegularPolicy(...));

// 일반 요금제에 기본 요금 할인 정책을 조합한 결과에 세금 정책 조합
Phone phone = new Phone(
        new TaxablePolicy(0.05, 
            new RateDiscountablePolicy(Money.wons(1000),
                new RegularPolicy(...)));

// 심야 요금 할인제에 기본 요금 할인 정책을 조합한 결과에 세금 정책 조합
Phone phone = new Phone(
        new TaxablePolicy(0.05, 
            new RateDiscountablePolicy(Money.wons(1000),
                new NightlyDiscountPolicy(...)));

새로운 정책 추가하기

객체 합성이 클래스 상속보다 더 좋은 방법이다

04 믹스인

기본 정책 구현하기

abstract class BasicRatePolicy { // 기본 정책
  def calculateFee(phone: Phone): Money = phone.calls.map(calculateCallFee(_)).reduce(_ + _)
  
  protected def calculateCallFee(call: Call): Money;
}

// 표준 요금제
class RegularPolicy(val amount: Money, val seconds: Duration) extends BasicRatePolicy {
  override protected def calculateCallFee(call: Call): Money = ...
}

// 심야 할인 요금제
class NightlyDiscountPolicy(
    val nightlyAmount: Money,  
    val regularAmount: Money,
    val seconds: Duration) extends BasicRatePolicy {   
  
  override protected def calculateCallFee(call: Call): Money = ... 
}

object NightltDiscountPolicy {
  val LateNightHour: Integer = 22
}

트레이트로 부가 정책 구현하기

// 세금 정책 트레이트
trait TaxablePolicy extends BasicRatePolicy {
  val taxRate: Double
  override def calculateFee(phone: Phone): Money = {
    val fee = super.calculateFee(phone)
    return fee + fee * taxRate
  }
}

부가 정책 트레이트 믹스인하기

class TaxableRegularPolicy(
    amount: Money, 
    seconds: Duration, 
    val taxRate: Double) 
  extends RegularPolicy(amount, seconds) // 조합될 클래스
  with TaxablePolicy // 믹스인할 클래스
class RateDiscountableAndTaxableRegularPolicy(
    amount: Money, 
    seconds: Duration, 
    val discountAmount: Money,
    val taxRate: Double)
  extends RegularPolicy(amount, seconds) // 표준 요금제
  with TaxablePolicy // 세금 정책을 적용 후
  with RateDiscountablePolicy // 비율 할인 정책 적용
  
class TaxableAndRateDiscountableRegularPolicy(
    amount: Money, 
    seconds: Duration, 
    val discountAmount: Money,
    val taxRate: Double)
  extends RegularPolicy(amount, seconds) // 표준 요금제에 
  with RateDiscountablePolicy // 비율 할인 정책 적용 후
  with TaxablePolicy // 세금 정책 적용
new RegularPlicy(Money(100), Duration.fSeconds(10))
    with RateDiscountPolicy
    with TaxablePolicy {
  val discountAmount = Money(100)
  val taxRate = 0.02
}

쌓을 수 있는 변경