상속보다는 컴포지션(조합)?? 을 사용하라 이게 대체 무슨 말인가? 책이랑 여러 블로그를 참고하면서 정리를 해 보려 한다
우선 책에서는
상속은 코드를 재사용하는 강력한 수단이지만 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트 웨어를 만들게 된다 . 이러한 문제는 상위 클래스와 하위 클랜스를 동일한 개발자가 개발하지 않는 경우 발생하게 된다 따라서 다른 클라이언트가 내가 만든 클래스를 상속 받을 수 있게 하려면 주의 해야한다
public class InstrumenteHashSetUseExtends<E> extends HashSet{
private int addCount = 0; // 추가된 원소의 수
public InstrumentedHashSetUseExtends() {
}
public InstrumentedHashSetUseExtends(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(Object o) {
addCount++;
return super.add(o);
}
@Override
public boolean addAll(Collection c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSetUseExtends<String> s = new InstrumentedHashSetUseExtends<>();
s.addAll(List.of("틱", "탁탁", "펑"));
System.out.println(s.getAddCount());
}
}
public static void main(String[] args) {
InstrumentedHashSetUseExtends<String> s = new InstrumentedHashSetUseExtends<>();
s.addAll(List.of("틱", "탁탁", "펑"));
System.out.println(s.getAddCount());
}
다음과 같이 addAll을 사용하여 추가할시 기대값으로 3개를 예상 할 것이다

허나 다음과 같이 6이 나왔다
예상값과 다르니 코드를 분석해볼 차례이다
@Override
public boolean addAll(Collection c) {
addCount += c.size();
return super.addAll(c);
}
현재 다음과 같이 super.addAll()를 보낸다
그래서 위로위로 타고 올라고 보기로 했다
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
// ...
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size() / .75f) + 1, 16));
addAll(c); // super의 addAll 메서드를 호출한다.
}
// ...
}
public abstract class AbstractCollection<E> implements Collection<E> {
// ...
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
// ...
}
결국에는
즉 super로 타고 올라갔더니
addAll(c); //HashSet
if (add(e)) // AbstractCollection<E>
@Override
public boolean add(Object o) {
addCount++;
return super.add(o);
} //InstrumentedHashSetUseExtends
이 것들 때문에 우리가 원하는 기대값을 받지 못하는 거였다
이경우 하위 addAll 메서드를 재정의하지 않으면 문제를 고칠 수 있다.
하지만 HashSet의 addAll 이 add 메서드를 이용해 구현했음을 가정한 해법이라 한계를 가진다
이처럼 자신의 다른 부분을 사용하는 자기 사용 여부는 해당 클래스의 내부 구혀 방식에 해당 한다
이러한 문제 외에도 상위 클래스에 새로운 메서드가 추가되었을 때와 같은 상황에서 다양한 문제가 발생할 수 있다.
이문제들을 해결할 수 있는 방안으로 바로 컴포지션이 있다.
컴포지션이 뭐냐
글로 보면 이해가 안되니 코드를 보자
public class InstrumentedHashSetUseComposition<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSetUseComposition(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;
}
}
전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public int size() {
return 0;
}
public boolean isEmpty() {
return s.isEmpty();
}
public boolean contains(Object o) {
return s.contains(o);
}
public Iterator<E> iterator() {
return s.iterator();
}
public Object[] toArray() {
return s.toArray();
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
public void clear() {
s.clear();
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
이런식으로 HashSet의 모든 기능을 정의한 Set 인터페이스를 활요해 설계되어 견고하고 유연하게 맏느다
임의의 Set 계측 기능을 덧쒸워 새로운 Set을 만드는 것이 이 클래스 핵심
즉 부모를 자식에 맞추어 새로 만들자 였다.
이러한 컴포지션의 장점으로는
상속은 상위클래스와 하위클래스가 is-a 관계가 완벽하게 성립하는지 확인하자
컴포지션을 써야할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노출하는 것과 같다
(클라이언트에서 노출된 내부에 직접 접근)
(최악의 경우 클라이언트에서 상위 클래스를 직접 수정하여 하위클래스이 불변식을 해칠수도 있다)
상속은 상위클래스 api 결함 까지도 승계함으로 주의
래퍼 클래스로 구현할 적당한 인터페이스가 있을시 고민할 것도 없이 컴포지션 사용하자