為什么更推薦使用組合而非繼承關(guān)系?
?前言
最近在看公司項目的代碼,看到了大量的繼承體系,而且還是繼承了多層,維護、閱讀都十分的困難。在查閱了一些資料以后,包括《Effective Java》一書中的第16條提到“組合優(yōu)先于繼承”。那繼承到底會暴露什么問題呢?為什么更推薦優(yōu)先使用組合呢?
繼承帶來的問題
老實講,項目中為什么大量使用繼承,估計初版設(shè)計的人是想實現(xiàn)代碼的復(fù)用,但是的確帶來不少的問題。
繼承是面向?qū)ο笾匾匦灾?,語義上是表達 is-a的關(guān)系,但是它會破壞封裝性。我們舉個例子:
假設(shè)我們要設(shè)計一個關(guān)于鳥的類。我們將“鳥類”這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird?,默認(rèn)有eat吃東西的行為。所有更細(xì)分的鳥,比如麻雀、鴿子、鴕鳥等,都繼承這個抽象類。
但是,這時候搞不清楚情況的人根據(jù)需求給AbstractBird?添加一個fly()的行為。但是對于鴕鳥這個子類來說,并不會飛,你如果不做任何處理,相當(dāng)于讓鴕鳥有了飛翔的功能,不符合設(shè)計。聰明的你想到了,那就重寫以下吧,拋出一個異常,如下所示:
這種設(shè)計思路雖然可以解決問題,但不夠優(yōu)美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對于這些不會飛的鳥來說,我們都需要重寫 fly()?方法,拋出異常。而且真正好的設(shè)計,對于鴕鳥和企鵝來說,就不應(yīng)該暴露給他們fly()這種不該暴露的接口,增加外部調(diào)用的負(fù)擔(dān)。
這里只提到了fly()?,如果還有下蛋egg()?、唱歌sing()這么多行為,總不能都冗雜在父類里吧。關(guān)鍵像我們的項目同事,基本上把所有的類都寫到了父類中,真的特別難以維護。
小結(jié)一下繼承帶來的問題:
子類繼承了父類所有的行為,會讓子類無意的暴露的不必要的接口,破壞封裝性。
如果繼承層級比較多,那么代碼的復(fù)雜度、可閱讀型就可想而知的難了。
另外一個點,就是非常不好做單元測試。
針對于這種問題,組合能怎么解決呢?
組合的好處
組合,顧名思意,就是把另外一個對象做成當(dāng)前這個對象的一部分,是組成我的一部分,它也能很好的實現(xiàn)代碼的復(fù)用,語義上表達的是has-a的意思,我有xxx的能力,我有xxx的功能。
那我們看看針對上面的例子,用組合的方式該如何實現(xiàn)呢?
- 定義接口
- 組合鴕鳥類
你看對于鴕鳥這個子類來說,只暴露了它有的能力,那就是eat?,沒有暴露fly的接口。
從理論上講,通過組合、接口、委托三個技術(shù)手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關(guān)系,特別是一些復(fù)雜的繼承關(guān)系。
繼承真的無用武之地了?
既然面向?qū)ο笾杏欣^承這玩意,說明它并非一無是處的。
如果類之間的繼承結(jié)構(gòu)穩(wěn)定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關(guān)系),繼承關(guān)系不復(fù)雜,我們就可以大膽地使用繼承。反之,系統(tǒng)越不穩(wěn)定,繼承層次很深,繼承關(guān)系復(fù)雜,我們就盡量使用組合來替代繼承。
除此之外,還有一些設(shè)計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern?)、策略模式(strategy pattern?)、組合模式(composite pattern?)等都使用了組合關(guān)系,而模板模式(template pattern)使用了繼承關(guān)系。
總結(jié)
不知道大家項目中繼承用的多嗎?其實在JDK中就有許多違反這條原則的地方,比如棧Stack?類并不是Vector?,不應(yīng)該有繼承關(guān)系,但是實際上就是繼承自Vector。不管如何,在項目中決定使用繼承而不是組合前,一定要考慮清楚,子類是否真的是父類的子類型?以后父類會不會經(jīng)常變動的可能?父類的某些API是否存在缺陷,如果有的話也會隨著子類擴散出去。