Item 16. 계승하는 대신 구성하라
interface를 계승할 때는 해당하지 않는 내용이다.
계승은 강력한 도구 이지만 캡슐화 원칙을 침해하므로 문제를 발생시킬 소지가 있다는 것이다.
계승은 캡슐화(encapsulation) 원칙을 위반한다.
- 하위 클래스가 정상 동작하기 위해서는 상위 클래스에 의존할 수 밖에 없다.
- 상위 클래스의 구현이 릴리스(release)가 거듭되면서 바뀔 수 있는데, 그러다 보면 하위 클래스 코드는 수정된 적이 없어도 망가질 수 있다.
망가질 수 있는, 깨질 수 있는 클래스
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
{ 생성자 생략 .. }
@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"));
3건을 추가하면 size가 3일 것 같지만 놉. HashSet의 addAll이 내부적으로 add를 이용해서 구현하기 때문.
- InstrumentedHashSet의 addAll() 메소드 호출하여 집계. addCount=3
- HashSet의 addAll()을 호출. HashSet의 메서드는 합입할 원소마다 add 메서드 호출
- InstrumentedHashSet의 add를 3번 호출. addCount=6
- HashSet의 add 메서드 호출
2가지의 문제점이 도출되는데, 메소드 재정의를 안하면 괜찮을 것이라는 판단을 할 수 있다. 하지만 위험성이 완전히 사라지는 것은 아니다
- 재정의한 addAll을 삭제하면 해결은 가능함. 이 해결 방법은 addAll이 add를 기반으로 구현했다는 사실에 근거한 것인데, 이는 릴리스가 거듭되며 바뀔 수 있지..
- 다음 릴리스 때 상위 클래스에 새로운 메소드가 추가될 수도 있다
계승대신 구성하라!
- 구성(composition) - 위 언급된 문제를 피하는 방법은, 계승 대신 새로운 객체에 기존 클래스 객체를 참조하는 private 필드를 만드는 것!
- 전달(forwarding) - 기존 클래스의 메소드를 호출하여 그 결과를 반환하는 것!
재사용 가능한 전달 클래스
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } public boolean add(E e) { return s.add(e); } public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } public boolean isEmpty() { return s.isEmpty(); } { .. 나머지 메서드 생략 .. } @Override public String toString() { return s.toString(); } }
-> 기존 클래스의 구현 세부사항에 종속되지 않기 때문에 견고하다
상속받는 포장 클래스(wrapper)
public class InstrumentedSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedSet(Set<E> s) { super(s); } @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; } }
-> 역호출 프레임워크(자기자신에 대한 참조를 다른 객체에 넘겨, 나중에 필요할 때 역호출 하도록 요청하는 것. 포장 객체는 포장 객체에 대해서 모르기 때문에 자기 자신에 대해서 전달 할 것이다. 따라서 역호출 과정에서는 포장 객체는 제외)에는 적합하지 않음.
계승은 언제 해야 할까?
- 상속은 하위 클래스가 상위 클래스의 하위 자료형(subtype)이 확실한 경우에만 바람직
- 클래스 B는 클래스 A와 "IS-A"관계가 성립할 때만 A를 계승 해야 한다. "아니다" 라면 B안에 A객체를 참조하는 private 필드를 두라!