오브젝트 책을 스터디 하는과정에서 정리한 글입니다.
자식클래스의 정의에 부모클래스의 이름을 덧붙이는 것만으로 부모 클래스의 코드를 재사용 할수 있음
취약한 기반 클래스 문제
라고 부름 이 문제를 해결하는 방법은 자식클래스와 부모클래스가 동시에 추상클래스
에 의존하도록 만드는 것.
public interface
에 의존 (캡슐화가 지켜짐) 상속보다 합성을 이용하는 것이 구현관점에서 번거롭고 복잡하지만, 설계는 변경과 관련된 것이라는 점을 기억하면 변경에 유연하게 대처할 수 있는 설계가 정답일 가능성이 높다
.
상속 대신 합성을 사용하면, 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경할 수 있다.
코드 재사용을 위해서는 객체 합성이 클래스 상속보다 더 좋은 방법이다.
합성을 사용하면 세가지 문제점을 해결 할 수 있음
- 불필요한 인터페이스 상속 문제 (Stack , Vector)
- 메소드 오버라이딩 오작용 문제 (addCount , addAll)
- 부모 클래스와 자식 클래스의 동시 수정 문제 (Playlist, PersonalPlaylist)
properties.put("viewrain", 12345);
assertEquals("12345", properties.getProperty("viewrain)); // 에러발생
import java.util.Hashtable;
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);
}
}
String 타입의 key, value 만 허용하는 규칙을 어길 위험은 사라진다.
Properties 는 Hashtable 의 내부 구현에 알지 못함
Stack 의 문제 (10장)
import java.util.EmptyStackException;
import java.util.Vector;
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 new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
}
import java.util.Collection;
import java.util.HashSet;
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
// @Override
// public boolean addAll(Collection<? extends E> c) {
// addCount += c.size();
// return super.addAll(c);
// }
@Override // hashset 의 addAll 과 동일한 문제는 여전히 존재
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
public int getAddCount() {
return addCount;
}
}
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.Spliterator;
public class InstrumentedHashSet<E> implements Set<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
@Override
public boolean add(E e) {
addCount++;
return set.add(e);
}
@Override
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.remove(o);
}
...
}
Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달함.
-> 포워딩 (forwarding)
동일한 메서드를 호출하기 위해 추가된 메서드 -> 포워딩 메서드(forwarding method)
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Playlist {
private List<Song> tracks = new ArrayList<>();
private Map<String, String> singers = new HashMap<>();
public void append(Song song) {
tracks.add(song);
singers.put(song.getSinger(), song.getTitle());
}
public List<Song> getTracks() {
return tracks;
}
public Map<String, String> getSingers() {
return singers;
}
}
public class PersonalPlaylist extends Playlist {
public void remove(Song song) {
getTracks().remove(song);
getSingers().remove(song.getSinger());
}
}
// 안타깝게도 Playlist 의 경우에는 합성으로 변경해도, 가수별 노래 목록을 유지하기 위해 Playlist와 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());
}
}
현재 시스템은 일반요금제와 심야 할인 요금제 두가지 종류의 요금제가 존재
기본정책 + 부가정책 => 핸드폰 요금제
조합가능한 모든 요금 계산 순서 (p.355)
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());
} else {
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
}
public class TaxableRegularPhone extends RegularPhone {
private 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));
}
}
부모 클래스의 메소드를 재사용하기 위해 super 호출을 사용하면, 원하는 결과는 쉽게 얻지만, 결합도가 높아짐
Phone 클래스에 새로운 추상메소드인 afterCalculated 를 추가하면, 자식 클래스에게 전체요금을 계산한 후에, 수행할 로직을 추가할 수 있는 기회를 제공한다.
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);
// 요기 이렇게 하면, RegularPhone 클래스는 요금수정이 필요없으므로,
}
public class RegularPhone extends Phone {
@Override
protected Money afterCalculated(Money fee){
return fee;
} // 이렇게 전달받은 그대로 넘겨주면 되고, 심야할인요금제도 마찬가지이다.
}
public abstract class Phone {
...
protected Money afterCalculated(Money money) {
return fee; // hook Method 라고 부름
}
...
}
public class TaxableRegularPhone extends RegularPhone {
private 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)); // 요기, 심야도 마찬가지로 해준다.
}
}
일반요금제와 기본요금 할인 정책을 조합
심야할인 요금제와 기본요금 할인정책을 조합
But 중복코드가 있다.
부가정책은 자유롭게 조합할 수 있어야 하고, 적용되는 순서 역시 임의로 결정할 수 있어야 함 (p.355 그림 11.2 다시한번 봐보기)
위에서 제시한 상속을 이용한 해결방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다.
public class TaxableAndRateDiscountableRegularPhone extends TaxableRegularPhone {
private 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 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));
}
}
이를 클래스 폭팔 (class explosion) 또는 조합의 폭발 (combinational explosion) 이라 부른다.
이 현상의 근본적인 문제는 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생한다.
합성은 컴파일타임 관계를 런타임 관계로 변경
함으로써, 이 문제를 해결한다. 실행시점에 정책들의 관계를 유연하게 변경, 실행시점에 인스턴스를 조립하는 방법
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 {
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());
}
}
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);
}
}
Phone 이 다양한 요금정책과 협력할 수 있어야 하므로, 요금 정책의 타입이 RatePolicy 라는 인터페이스로 정의되 있는걸 주목해야 한다.
의존성 주입을 사용해 런타임에 필요한 객체를 설정하는 것이 일반적임.
p.372 그림 11.7 합성 관계를 사용한 기본 정책의 전체적인 구조 봐보기
일반요금제의 경우 합성
Phone phone = new Phone(new RegularPolicy(Money.wons(10), Duration.ofSeconds(10)));
Phone phone = new Phone(new NightlyDiscountPolicy(Money.wons(5), Money.wons(10) , Duration.ofSeconds(10)));
-> 이제 필요에 따라 적절한 인스턴스를 생성하기만 하면 된다.
public abstract class AdditionalRatePolicy implements RatePolicy { // RatePolicy 의 역할을 수행하므로, RatePolicy 인터페이스를 구현함
private RatePolicy next;
// 컴파일 타임 의존성을 쉽게 대체할 수 있도록, RatePolicy 타입의 인스턴스를 인자로 받는 생성자를 제공
public AdditionalRatePolicy(RatePolicy next) {
this.next = next; // next에 전달된 인스턴스에 대한 의존성 주입
}
@Override
public Money calculateFee(Phone phone) {
// next가 참고하고 있는 인스턴스에게 calculateFee 메시지를 전송
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);
}
}
p.375 그림 11.11 기본 정책과 부가 정책을 조합할 수 있는 상속구조
Phone phone = new Phone(new TaxablePolicy(0.05, new RegularPolicy());
Phone phone = new Phone( new TaxablePolicy(0.05, new RateDiscountablePolicy(Money.wons(1000),RegularPolicy(...))));
코드를 재사용하기 위해 널리 사용되는 방법은 상속이다.
건전한 결합도를 유지하는 방법은 합성이다.
그럼 상속을 사용하지마?
스칼라에서 제공하는 트레이트(trait)를 이용해 믹스인을 구현해보자
기본정책을 추상클래스로.
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 =
amount*(call.duration.getSeconds / seconds.getSeconds)
}
class NightlyDiscountPolicy (
val nightlyAmount: Money,
val regularAmount: Money,
val seconds: Duration) extends BasicRatePolicy {
override protected def calculateCallFee(call: Call): Money =
if(call.from.getHour >= NightlyDiscountPolicy.LateNightHour){
nightlyAmount*(call.duration.getSeconds / seconds.gerSeconds)
}else{
regularAmount*(call.duration.getSeconds / seconds.getSeconds)
}
}
object NightlyDiscountPolicy{
val LateNightHour: Integer = 22
}
// TaxablePolicy 트레이트가 BasicRatePolicy 를 확장한다는 점에 주목
trait TaxablePolicy extends BasicRatePolicy {
def texRate: Double
override def calculateFee(phone: Phone): Money = {
val fee = super.calculateFee(phone)
return fee + fee * taxRate
}
}
위 코드에서 extends 는 단지 TaxablePolicy가 사용될 수 있는 문맥을 제한할 뿐
TaxablePolicy는 BasicRatePolicy를 상속받은 경우에만 믹스인 될 수 있다.
* 상속의 개념이 아니라, TaxablePolicy가 BasicRatePolicy 나 BasicRatePolicy 의 자손에 해당하는 경우에만 믹스인 될수 있다는 것을 의미
RegularPolicy와 NightlyDiscountPolicy 에 믹스인 될수 있으며 심지어 미래에 추가될 새로운 BasicRatePolicy의 자손에게도 믹스인 될 수 있지만, 다른 클래스나 트레이트에는 믹스인 될 수 없다.
class TaxableRegularPolicy {
amount: Money,
seconds: Duration,
val taxRate: Double)
extends RegularPolicy(amount, seconds)
with TaxablePolicy
}
추상 서브클래스 abstract subclass
(믹스인의 또다른 불림) 쌓을수 있는 변경 stackable modification