2020/07/20

[筆記] Effective Java #18 複合優先於繼承

Effective Java 3rd 簡體中文版筆記 #18 複合優先於繼承
繼承 (inheritance) 是 reuse 程式碼的一種方式,本篇討論的繼承主要為 extends,不包含實作介面 (implement interface) 的部分。繼承與調用方法不同的是它打破封裝性,super class 實作如果改變,subclass 即使沒有變,依然會受到影響。
下面的程式範例為 InstrumentedHashSet,它會計算有多少元素新增,繼承 HashSet 來達到目的,並覆寫 addaddAll 方法,加入 addCount 計算邏輯。
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;
    public InstrumentedHashSet() {
    }
    ...
    @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;
    }
}
上述的程式非常合理,實際上是有問題的,只新增 3 個元素,但 addCount 卻是 6。原因為在 super class 使用 addAll 時,其實是使用 add 來協助完成的,在覆寫 add 後,addCount 就自然地再 +3。HashSet 的 addAll 會轉由 AbstractCollection 中的 addAll 來執行,原始碼如下。
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
s.getAddCount() //  6
InstrumentedHashSet
是脆弱的,因為它會受到 super class 影響,如果 super class 某次更新將 add 行為更改,subclass 有機會被破壞。那是否在繼承時不要覆寫方法,只新增方法就不會有問題? 或許,但還是無法保證完全不會有問題,如果 subclass 已新增的方法,在 super class 某次更新後,同樣新增該命名的方法,這時一樣又變成覆寫。
要解決問題,建議使用 composition (複合),在原 subclass 新增 private field,private field 就是本來的 super class,範例如下。所有的方法都調用原 super class 對應的方法,InstrumentedHashSet 是 wrapper class,將一個 Set 包裝起來,又稱作裝飾者 (Decorator) 模式,這類型的 class 不適合用在 callback function。
public class InstrumentedHashSet<E> {
    private int addCount = 0;
    private final Set<E> s;

    public InstrumentedHashSet(Set<E> s) {
        this.s = s;
    } 
    public boolean add(E e) {
        addCount++;
        return s.add(e);
    }
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return s.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }
}
Java 中有的 class 是違反這項規則的,像是 java.util.Stack 繼承 Vector,但兩者本身沒有關係。那什麼時候適合用繼承呢? 當 subclass 是一種 super class 時就適用 (is-a),如果 super class 不是為繼承而設計的,那麼就不應該繼承它,另外在使用繼承時,API 的缺點將會完整的曝露給 subclass,但如果用複合是有機會重新設計的。

轉載請註明原文網址 https://cookieandcoketw.blogspot.com/2020/07/effective-java-18-composition.html

沒有留言:

張貼留言