第16條:復合優于繼承
前言
本條內容的繼承不包括接口繼承。
1.什么事復合
復合就是在你的類中添加一個私有域,引用一個類的實例,使被引用類成為引用類的一個組件。
2. 繼承的缺點
(1)繼承不容易控制,使用不當容易導致軟件非常脆弱,特別是繼承不再同一個包下的類。
(2)繼承打破了父類的封裝性,有的時候父類的內部實現改變,可能會導致子類遭到破壞。
舉個比書上簡單一點的例子,比如我們有個類,他包含一個集合,我們要并對外提供了兩個api,分別是add(String str)和addAll(List<String> strs),具體的類如下:
public class MyObject {
private List<String> list = new ArrayList<>();
public void add(String ele) {
list.add(ele);
}
public void addAll(List<String> elements) {
for(String ele : elements) {
add(ele);
}
}
}
然后我們需要記錄這個類的從創建到銷毀,一共添加過多少元素,如果我們想要用繼承的方式,并且在不知道具體內部實現的前提之下,我們可能會這樣寫:
public class MyChildObject extends MyObject {
private int addedEleNum = 0;
@Overried
public void add(String ele) {
addedEleNum++;
super.add(ele);
}
@Overried
public void addAll(List<String> elements) {
addedEleNum += elements.size();
super.addAll(elements);
}
}
很明顯,這樣做是得不到我們想要的結果的,想要得到我們想要的結果,我們一般需要查看MyObject的具體實現,這就打破了封裝性,好吧,看了具體實現之后我們知道怎么做了,那就是不覆蓋addAll()方法。那問題又來了,如果在下一個版本中,MyObject的addAll()方法改了呢,改成想下面這樣的:
public void addAll(List<String> elements) {
for(String ele : elements) {
list.add(ele);
}
}
這樣的話MyChildObject又不能正常工作了,OMG。導致子類不能正常工作的原因還有很多,甚至父類中新添加一個類似add()的方法都會導致子類不能正常工作。所以這樣的子類是異常脆弱的。
so,可以被繼承的類要么在同一個包內(在同一個程序員的控制之下),要么是專門為繼承而實際,并提供了很好的文檔說明。
(3)看了第二點,你可能會覺得,我繼承的時候只要不覆蓋父類的方法不就可以了么?確實,相對于覆蓋確實安全一些,不過這不是絕對安全,當父類新增了一個方法,并且方法名和和參數都和父類相同,但返回值不同,那么子類將無法通過編譯。如果返回值也相同的話,又回到了第二個問題。同樣導致了子類不健壯。
3.復合的優點
上面說到繼承的缺點就是復合的優點。
4.復合的正確使用姿勢
在這里需要先解釋一下“轉發“的概念,轉發就是,你先復合一個類,然后在復合的類中實現所有被復合類的公有方法(api),實現的方式就是在相應的方法中調用被復合類的方法,并且不能被添加其他方法。比如為上面的MyObject寫一個轉發類:
public class ForwardingMyObject {
private MyObject mObject;
//這里使用依賴注入的方式來得到被復合類的引用
//目的是提高可測試性和靈活性
public ForwardingMyObject(Myobject object) {
this.mObject = object;
}
public void add(String ele) {
mObject.add(ele);
}
public void addAll(List<String> elements) {
mObject.addAll(elements);
}
}
轉發類就是上面提到的,專門為繼承而設計的類。
現在來闡述復合的正確使用姿勢:
(1)為想要被繼承的類設計一個轉發類。
(2)繼承這個轉發類。
(3)覆蓋想要覆蓋的方法,或者添加想要添加的方法。
將例子寫完,我們來快樂的繼承ForwardingMyObject
吧:
public class MyChildObject extends ForwardingMyObject {
private int count = 0;
public MyChildObject(MyObject object) {
super(object);
}
@Override
public void add(String ele) {
count ++;
super.add(ele);
}
@Override
public void addAll(List<String> elements) {
cout += elements.size();
super.addAll(elements);
}
}
為什么不直接在在轉發類中直接實現計數功能?這樣好麻煩!
好吧,我承認,上面的例子太簡單,不利于解釋這個問題,主要是為了便于理解,那我們繼續。
首先問個問題,我們在設計一個類的api的時候是直接在類中寫一堆public的方法么?
什么?是的,好吧,你這種沒追求的程序員快滾去睡覺吧,我不想和你聊天T_T。
我們在設計一個類的api的時候一般都是先將類的接口寫出來,然后在用這個類來實現這個接口。
行,明白這里我們就來看書里的栗子吧,這里我把set改成list,聯系下上文中的栗子:
轉發類:
public class ForwardingList<E> implements List<E> {
private List<E> mList;
public ForwardingList(List<E> list) {
this.mList = list;
}
@Override
public void add(int location, E object) {
mList.add(location, object);
}
@Override
public boolean add(E object) {
return mList.add(object);
}
//其他的一些api
...
}
包裝類(相當于上面例子中的MyChildObject):
public class InstrumentedList<E> extends ForwardingList<E> {
private int addCount = 0;
public InstrumentedList(List<E> list) {
super(list);
}
@Override
public boolean add(E object) {
addCount++;
return super.add(object);
}
@Override
public void add(int location, E object) {
addCount++;
super.add(location, object);
}
@Override
public boolean addAll(@NonNull Collection<? extends E> collection) {
addCount += collection.size();
return super.addAll(collection);
}
@Override
public boolean addAll(int location, @NonNull Collection<? extends E> collection) {
addCount += collection.size();
return super.addAll(location, collection);
}
public int getAddCount() {
return addCount;
}
}
看到好處了么?現在我們寫的InstrumentedList是一個真正List,不僅僅只是名字里有List而已!這意味著任何需要List作為參數的地方都可以把他傳遞過去!
不僅如此,它實現了一個傳入List的構造方法,也就是說只要是實現了List的類都可以傳遞進去,什么ArrayList呀LinkedList都可以傳進去并統計add了多少個元素。十分靈活!
5.復合的缺點
不適合用于回調框架。
6. 總結
簡而言之,繼承的功能十分強大,但也存在諸多問題,因為他違背了封裝原則。只有當子類和超類確實存在子類型關系時,使用繼承才是恰當的。即便如此,如果子類和超類存在不同的包中,并且超類并不是為繼承而設計的,那么繼承將導致脆弱性,可以用復合和轉轉發機制來代替繼承,尤其是當存在適當的接口可以實現包裝類的時候。包裝類不僅比子類更加健壯,而且功能也更加強大。