繼承時實現代碼重用的重要手段,但它並非永遠是完成這項工作的最佳工具,不恰當的使用會導致程式變得很脆弱,當然,在同一個程式員的控制下,使用繼承會變的非常安全。想到了很有名的一句話,你永遠不知道你的用戶是如何使用你寫的程式的,一個程式員繼承另一個程式員寫的類也是同樣的危險。 於方法調用不同的是,繼承打破 ...
繼承時實現代碼重用的重要手段,但它並非永遠是完成這項工作的最佳工具,不恰當的使用會導致程式變得很脆弱,當然,在同一個程式員的控制下,使用繼承會變的非常安全。想到了很有名的一句話,你永遠不知道你的用戶是如何使用你寫的程式的,一個程式員繼承另一個程式員寫的類也是同樣的危險。
於方法調用不同的是,繼承打破的封裝性。換句話說,子類依賴於其超類中特定功能的實現細節。超類的實現有可能隨著發行版本的不同而變化,如果真的發生了變化,子類可能會遭到破壞,即使它的代碼完全沒有修改過。因而,子類有必要隨著超類的更新而演變。
為了說明的更加具體一些,假設有一個程式使用了HashSet,想要查詢它被創建以來添加了多少個元素,編寫一個hashSet變數,它記錄下試圖插入的元素數量,並有一個訪問數量的方法
/** * 複合優先與繼承 * @author weishiyao * * @param <E> */ // 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; } }
看起來非常合理,但是不能正常工作,假設創建一個實例用addAll方法添加三個元素,
public static void main(String[] args) { InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(Arrays.asList("a", "b", "c")); System.out.println(s.getAddCount()); }
這時候我們期望的返回值應該是3,但是事與願違,結果是6,因為在HashSet內部,addAll方法是機遇add方法來實現,相當於每次插入我們都計算了2次,所以結果是6.
這個問題來源於Override這個動作,但是如果我們不覆蓋現有的方法,可能認為是安全的,雖然這種做法比較安全一些,但是,也並非沒有風險,如果超類在後續版本中增加了一個新的方法,並且不幸的是,這個方法如果和我們寫的方法重名,要麼是返回類型不同,這樣整個程式將會直接down掉,如果返回類型相同,問題返回上面。
這時候有一種方案可以避免以上的所有問題。不用擴展現有的類,而是在新的類中增加一個私有對象,它引用現有對象的一個實例,這種設計被稱作為複合,因為現有的類變成了新類的一個組件,新類中的每個實力方法都可以調用被包涵的現有類實例中對應的方法,並返回它的結果,這稱為轉發,新類中的方法被稱為轉發方法。
重新實現上面那個需求
/** * 複合優先與繼承 * @author weishiyao * * @param <E> */ // Reusable forwarding class 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(); } }
/** * 複合優先與繼承 * @author weishiyao * * @param <E> */ // 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++; return super.addAll(c); } public int getAddCount() { return addCount; } }
Set介面的存在使得InstrumentedSet類的設計成為可能,因為Set介面保存了HashSet類的功能特性。除了獲得健壯性外,這種設計也帶來了靈活性。InstrumentedSet類實現了Set介面,並擁有單個構造器,它的參數也是Set類型,從本質上來將,這個類把一個Set轉變成了另一餓Set,同時增加了計數的功能。
前面提到的基於繼承的方法只適用與單個具體的類,並且對於超類中所支持的每個構造器都要求有一個單獨的構造器,於此不同的是這裡的包裝類可以用來包裝任何Set實現,並且可以結合任何以前存在的構造器一起工作。例如:
Set<Date> s1 = new InstrumentedSet<>(new TreeSet<Date>()); Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>());
因為每一個InstrumentedSet實例都把另一個Set實例包裝起來了,所有InstrumentedSet類被稱為包裝類。這也正是裝飾器模式,因為InstrumentedSet類對一個集合進行了裝飾,為它增加了計數特性。