項目 16. 偏愛組合勝過繼承
繼承是違反封裝的。子類別的功能是依據父類別實作的細節而定的,每次更版都有可能變動,除非父類別的作者在文件有明確其繼承的目的,否則,子類別也需要同步更動。
// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
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;
}
}
假設我們有一個繼承HashSet的類別叫:InstrumentedHashSet,addCount是用來記錄其成員筆數,但是實際執行時確發現,原本我只是呼叫addAll()方法,但是為什麼成員的筆數確成倍增加,例如:我一次新增3筆資料進去,但addCount的結果確是6不是3,這是因為在父類別HashSet的方法中,addAll()方法裡又呼叫了add()方法。
另外,父類別在之後的版本可以呼叫新的方法。假設有一段程式依賴其安全性,每個成員在新增到新的集合時皆滿足某個述語,但如果父類別在新版呼叫新的方法新增成員,而子類別沒有覆寫,就會造成安全性的漏洞。
你可能覺得,如果你繼承了一個類別且極少覆寫方法來新增成員的寫入,就比較安全,但是,假設父類別新增方法的signature剛好跟你子類別自訂義的方法一樣,但回傳值不同,那你的子類別就無法compile了。
幸運的是,有一個方法可避免上述的問題。相較於繼承既有的類別,你可以在新類別新增一個private屬性並指向既有類別的實體,這樣的設計叫composition,因為既有類別就成為了新類別裡的成員了。在新類別中,每個實體方法會呼叫相對應既有類別的方法回傳結果,而這叫做forwarding,其結果是不會變的,因為和既有類別的實作並無相依性。即使既有類別新增方法也不影響新的類別。
// Wrapper class - uses composition in place of inheritance
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;
}
}
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty()
public int size()
public Iterator<E> iterator()
public boolean add(E e)
public boolean remove(Object o)
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override
public boolean equals(Object o){ return s.equals(o); }
@Override
public int hashCode() { return s.hashCode(); }
@Override
public String toString() { return s.toString(); }
}
InstrumentedSet 類別被認為是wrapper類別,因為每個InstrumentedSet類別包含一個Set類別的實體。這也可稱做是Decorator pattern,而有時候,composition+forwarding可廣義視為delegation。但技術上,它不是,除非wrapper物件將自己傳遞到被wrapped的物件。
wrapper類別的缺點很少,但有一項該注意的是:wrapper類別不適用在callback框架,因為被wrapper的物件並不知道自己的wrapper,他將參考傳給自己並且callback這就會抵消了他的wrapper。有些人擔心forwarding方法對效能的影響還有wrapper物件對記憶體的佔用, 但實務上並不會。撰寫forwarding方法很冗長,但每個介面只需要寫一次。