16. 계승하는 대신 구성하라.
- 계승은 상위 클래스와 하위 클래스 구현을 같은 프로그래머가 통제하는 단일 패키지 안에서 사용 하면 안전하다.
- 계승을 고려해서 설계되고 그에 맞는 문서를 갖춘 클래스에 사용하는 것 도 안전하다.
메서드 호출과 달리, 계승은 캡슐화(encapsulation)원칙을 위반한다.
[계승의 잘못된 사례]
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);
}
public int getAddCount(){
return addCount;
}
}
위와 같이 구현하고, 아래와 같이 사용하면 기대하던 값이 나오지 않는다.
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop");
s.getAddCount()값으로 기대되는 값은 3이지만, 실제로 6을 리턴한다. HashSet의 addAll메서드는 내부 구현이 add메서드로 되어 있으나, 그에 대한 명세가 HashSet문서에 정의되어 있지 않다.
InstrumentedHashSet에 정의된 addAll메서드는 addCount에 3을 더하고 상위클래스인 HashSet의 addAll 메서드를 super.addAll과 같이 호출하는데, 이 메서드는 InstrumentedHashSet에서 재정의한 add메서드를 삽입할 원소마다 호출하게 된다.
이런 구현은 HashSet의 구현 세부 사항이기 때문에 자바 플랫폼마다 똑같이 구현되었으리라 볼 수 없다.
따라서, InstrumentedHashSet 클래스는 깨지기 쉬운(fragile)클래스일 수밖에 없다.
규칙 16. 계승하는 대신 구성하라. (112.p ~ 113.p)를 다시 한번 꼭 읽어 볼것!!!
기존 클래스를 계승하는 대신, 새로운 클래스에 기존 클래스 객체를 참조하는 private 필드를 하나 추가하면, 이 문제를 회피할수 있다. 이런 설계 기법을 구성(composition)이라고 부르는데, 기존 클래스가 새 클래스의 일부(component)가 되기 때문이다.
[재사용 가능한 전달(forwarding) 클래스로 구현한 사례]
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s){ this.s = s; }
@Override public int size() { return s.size(); }
@Override public boolean isEmpty() { return s.isEmpty(); }
@Override public boolean contains(Object o) { return s.contains(o); }
@Override public Iterator<E> iterator() { return s.iterator(); }
@Override public Object[] toArray() { return s.toArray(); }
@Override public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean add(E e) { return s.add(e); }
@Override public boolean remove(Object o) { return s.remove(o); }
@Override public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
@Override public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
@Override public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
@Override public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
@Override public void clear() { s.clear(); }
}
규칙 16. 계승하는 대신 구성하라. (115.p ~ 116.p)를 다시 한번 꼭 읽어 볼것!!!
계승은 하위 클래스가 상위 클래스의 하위 자료형(subType)이 확실한 경우에만 사용하자.!!!!
구성 대신 계승을 사용하려 할때 반드시 물어야 하는 질문들.
- 계승할 클래스의 API에 문제가 있는 가?
- 그렇다면, 그 문제들이 계속 새 API의 일부가 되어도 상관없는가?
계승 메커니즘은 상위 클래스의 문제를 하위 클래스에 전파시킨다. 반면 구성 기법은 그런 약점을 감추는 새로운 API를 설계할 수 있도록 해준다.