項目 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方法很冗長,但每個介面只需要寫一次。

results matching ""

    No results matching ""