讀《Effective java 中文版》(15)
第14條:複合優先於繼承
繼承是實現代碼重用的有力手段,但不適當地使用繼承會導致脆弱的軟體。
- 在一個包內使用繼承是非常安全的,因在那兒子類和超類在同一個程式員的控制之下。
- 對於專門為了繼承而設計、且有很好文檔說明的類,使用繼承也是安全的。
- 對普通的具體類(concret class)時行跨躍包邊界的繼承,則是非常危險的。
與方法調用不同的是,繼承打破了封裝性,一個子類依賴於其超類中特定功能的實現細節。超類的實現的變化,則子類可能會被打破。
- 自用性,即超類中不同公有方法的實現時存在調用關係,當子類改寫這些方法時,常會導致子類的脆弱
- 超類在它的後續方法中增加了新的方法,如果子類不能及時改寫這些方法,異常的資料或操作出現。
- 子類繼承超類後,增加了一個新的方法,則當超類在新版中也增加了具有相同原型特徵的方法時,可能會出現問題
有一種辦法可以避免上述所有問題:新類,不是擴充一個已有類,而是設定一個私人域,它引用這個已有類的一個執行個體。這種設計被稱為“複合(composition)”。新類中的每個執行個體方法,都可以調用被包含的已有類執行個體中對應的方法,並返回它的結果,即為“轉寄方法(forwarding method)”。這樣的類比較穩固,這不依賴於已有類的實現細節。一個類的執行個體都把另一個類的實現封裝起來了,則前者的類叫做封裝類(wrapper class)。
看一個例子:
//wrapper class - uses composition in place of inheritance
public class InstrumentedSet implements Set{
private final Set s;
private int addCount=0;
public InstrumentedSet(Set s){
this.s=s;
}
public boolean add(Object o){
addCount++;
return s.add();
}
public boolean addAll(Collection c){
addCount+=c.size();
return s.addAll(c);
}
public int getAddCount(){
return addCount;
}
//forwarding methods
public void clear() { s.clear(); }
public boolean contains(Object o){return s.contains(o); }
public boolean isEmpty() {return s.isEmpty(); }
public int size() {return s.size(); }
....
public String toString() {return s.toString(); }
}
上例中,InstrumentedSet類對Set類進行了修飾,增加了計數特性。有時,複合和轉寄這兩項技術的結合被錯誤地引用為“委託(delegation)”,從技術的角度而言,這不是委託,除非封裝對象把自己傳遞給一個被封裝的對象。
封裝類幾乎沒有什麼缺點。需要注意的是,封裝類不適合用於回調架構(callback framework)中。在回調架構中,對象把自己的引用傳遞給其它的對象,以便將來調用回來,當它被封裝起來以後,它並不知道外面的封裝對象的情況,所以它傳遞一個指向自己的引用(this)時,會造成回調時繞開外面的封裝對象的問題。這被稱為SELF問題。
只有當子類真正是超類的“子類型(subtype)”時,繼承才是合適的。即兩者之存在“is-a”的關係。java平台中,也有違反這條規則的地方:如Stack不是向量,所以不應擴充Vector;屬性列表不是散列表,所以Properties不應擴充Hashtable。在決定使用複合還是擴充時,還要看一試圖擴充的類的API有沒有缺陷,如果你願意傳播這些缺陷到自己的API中,則用繼承,否則可用複合來設計一個新的API。
Posted by Hilton at February 14, 2004 09:21 PM | TrackBack