前言
最近在看公司專案的程式碼,看到了大量的繼承體系,而且還是繼承了多層,維護、閱讀都十分的困難。在查閱了一些資料以後,包括《Effective Java》一書中的第16條提到“組合優先於繼承”。那繼承到底會暴露什麼問題呢?為什麼更推薦優先使用組合呢?
歡迎關注微信公眾號「JAVA旭陽」交流和學習
繼承帶來的問題
老實講,專案中為什麼大量使用繼承,估計初版設計的人是想實現程式碼的複用,但是的確帶來不少的問題。
繼承是物件導向重要特性之一,語義上是表達 is-a
的關係,但是它會破壞封裝性。我們舉個例子:
假設我們要設計一個關於鳥的類。我們將“鳥類”這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird
,預設有eat
吃東西的行為。所有更細分的鳥,比如麻雀、鴿子、鴕鳥等,都繼承這個抽象類。
public class AbstractBird {
//... 省略其他屬性和方法...
public void eat() { //... }
}
// 鴕鳥
public class Ostrich extends AbstractBird {
}
但是,這時候搞不清楚情況的人根據需求給AbstractBird
新增一個fly()
的行為。但是對於鴕鳥這個子類來說,並不會飛,你如果不做任何處理,相當於讓鴕鳥有了飛翔的功能,不符合設計。聰明的你想到了,那就重寫以下吧,丟擲一個異常,如下所示:
public class AbstractBird {
//... 省略其他屬性和方法...
public void eat() { //... }
public void fly() { //... }
}
// 鴕鳥
public class Ostrich extends AbstractBird {
//... 省略其他屬性和方法...
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
}
這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,我們都需要重寫 fly()
方法,丟擲異常。而且真正好的設計,對於鴕鳥和企鵝來說,就不應該暴露給他們fly()
這種不該暴露的介面,增加外部呼叫的負擔。
這裡只提到了fly()
,如果還有下蛋egg()
、唱歌sing()
這麼多行為,總不能都冗雜在父類裡吧。關鍵像我們的專案同事,基本上把所有的類都寫到了父類中,真的特別難以維護。
小結一下繼承帶來的問題:
- 子類繼承了父類所有的行為,會讓子類無意的暴露的不必要的介面,破壞封裝性。
- 如果繼承層級比較多,那麼程式碼的複雜度、可閱讀型就可想而知的難了。
- 另外一個點,就是非常不好做單元測試。
針對於這種問題,組合能怎麼解決呢?
組合的好處
組合,顧名思意,就是把另外一個物件做成當前這個物件的一部分,是組成我的一部分,它也能很好的實現程式碼的複用,語義上表達的是has-a
的意思,我有xxx的能力,我有xxx的功能。
那我們看看針對上面的例子,用組合的方式該如何實現呢?
- 定義介面
public interface Eatable {
void eat();
}
public interface Flyable {
void fly();
}
public class EatAbility implements Eatable {
@Override
public void eat() {
System.out.println("I can eat");
}
} // 省
public class FlyAbility implements Flyable {
@Override
public void fly() {
System.out.println("I can fly");
}
} // 省
- 組合鴕鳥類
public class Ostrich implements Eatable {// 鴕鳥
private Eatable eatable = new EatAbility(); // 組合
//... 省略其他屬性和方法...
@Override
public void eat() {
eatable.eat(); // 委託
}
}
你看對於鴕鳥這個子類來說,只暴露了它有的能力,那就是eat
,沒有暴露fly
的介面。
從理論上講,透過組合、介面、委託三個技術手段,我們完全可以替換掉繼承,在專案中不用或者少用繼承關係,特別是一些複雜的繼承關係。
繼承真的無用武之地了?
既然物件導向中有繼承這玩意,說明它並非一無是處的。
如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不復雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,我們就儘量使用組合來替代繼承。
除此之外,還有一些設計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern
)、策略模式(strategy pattern
)、組合模式(composite pattern
)等都使用了組合關係,而模板模式(template pattern
)使用了繼承關係。
總結
不知道大家專案中繼承用的多嗎?其實在JDK中就有許多違反這條原則的地方,比如棧Stack
類並不是Vector
,不應該有繼承關係,但是實際上就是繼承自Vector
。不管如何,在專案中決定使用繼承而不是組合前,一定要考慮清楚,子類是否真的是父類的子型別?以後父類會不會經常變動的可能?父類的某些API是否存在缺陷,如果有的話也會隨著子類擴散出去。
歡迎關注微信公眾號「JAVA旭陽」交流和學習
更多學習資料請移步:程式設計師成神之路