메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
- 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
public class InheritSet<E> extends HashSet<E> {
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;
}
}
InheritSet<String> s = new InheritSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));
assertThat(s.getAddCount()).isEqualTo(3) // false
assertThat(s.getAddCount()).isEqualTo(6) // true
HashSet의 addAll은 내부적으로 각 원소를 add 메서드를 호출해 추가한다.
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
따라서 재정의된 addAll을 호출했을 때 addCount에 3을 더한후, 재정의 된 add를 호출하여 addCount에 중복으로 더해지게 된다.
기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.
public class CustomSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public CustomSet(Set<E> set) {
super(set);
}
@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;
}
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> set;
public ForwardingSet(Set<E> set) {this.set = set;}
@Override
public int size() {return set.size();}
@Override
public boolean isEmpty() {return set.isEmpty();}
@Override
public boolean contains(Object o) {return set.contains(o);}
@Override
public Iterator<E> iterator() {return set.iterator();}
@Override
public Object[] toArray() {return set.toArray();}
@Override
public <T> T[] toArray(T[] a) {return set.toArray(a);}
@Override
public boolean add(E e) {return set.add(e);}
@Override
public boolean remove(Object o) {return set.remove(o);}
@Override
public boolean containsAll(Collection<?> c) {return set.containsAll(c);}
@Override
public boolean addAll(Collection<? extends E> c) {return set.addAll(c);}
@Override
public boolean retainAll(Collection<?> c) {return set.retainAll(c);}
@Override
public boolean removeAll(Collection<?> c) {return set.removeAll(c);}
@Override
public void clear() {set.clear();}
@Override
public boolean equals(Object o) {return set.equals(o);}
@Override
public int hashCode() {return set.hashCode();}
}
CustomSet은 Set 인터페이스를 implements한 ForwardingSet을 상속한다.
따라서 다음 상황에서 HashSet의 부모 클래스에 영향을 받지 않고 오버라이딩을 할 수 있다.
Set<String> s = new CustomSet<>(new HashSet<>());
s.addAll(List.of("틱", "탁탁", "펑"));
assertThat(s.getAddCount()).isEqualTo(3) // true
기존 클래스를 확장 하는 대신, 새로운 클래스를 만들고 private필드로 기존 클래스의 인스턴스를 참조하게 하는 설계를 컴포지션이라 한다.
새 클래스의 메소드들은 private 필드로 참조하는 기존 클래스의 대응하는 메소드를 호출해 그 결과를 반환하게 한다.
이러한 방식을 전달(forwarding)이라 하고, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 한다.
CustomSet은 다른 Set 인스턴스를 감싸고 있다는 뜻에서 래퍼 클래스라 한다.
또한 이러한 패턴을 다른 Set에 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다.
상속은 하위 클래스가 상위 클래스의 관계가 완벽한 is-a 관계일 때만 사용하자.
하지만 대부분의 상황에서 변화가 일어날 수 있기 때문에 is-a 관계로 명확히 판단할 수 있는 경우는 많지 않다.
따라서 상속을 사용할 수 없는 경우에는 컴포지션을 사용하는 것이 좋은 해결책이 될 수 있다.