組合與繼承改選擇誰?

2021-10-23 00:19:50 字數 2721 閱讀 6604

在物件導向程式設計中,有一條非常經典的設計原則,那就是:組合優於繼承,多用組合少用繼承。為什麼不推薦使用繼承?組合相比繼承有哪些優勢?如何判斷該用組合還是繼承?

繼承是物件導向的四大特性之一,用來表示類之間的 is-a 關係,可以解決**復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到**的可維護性。所以,對於是否應該在專案中使用繼承,網上有很多爭議。很多人覺得繼承是一種反模式,應該盡量少用,甚至不用。為什麼會有這樣的爭議?我們通過乙個例子來解釋一下。假設我們要設計乙個關於鳥的類。將「鳥類」這樣乙個抽象的事物概念,定義為乙個抽象類 abstractbird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。大部分鳥都會飛,那我們可不可以在 abstractbird 抽象類中,定義乙個 fly() 方法呢?答案是否定的。儘管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有「飛」這樣的行為,這顯然不符合我們對現實世界中事物的認識。當然,在鴕鳥這個子類中重寫(override)fly() 方法,讓它丟擲 unsupportedmethodexception 異常不就可以了嗎?

這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,都需要重寫 fly() 方法,丟擲異常。這樣的設計,一方面,徒增了編碼的工作量;另一方面,也違背了最小知識原則(least knowledge principle,也叫最少知識原則或者迪公尺特法則),暴露不該暴露的介面給外部,增加了類使用過程中被誤用的概率。

那我們再通過 abstractbird 類派生出兩個更加細分的抽象類:會飛的鳥類 abstractflyablebird 和不會飛的鳥類 abstractunflyablebird,讓麻雀、烏鴉這些會飛的鳥都繼承 abstractflyablebird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 abstractunflyablebird 類,不就可以了嗎?

但是這樣做,繼承關係變成了三層。不過,整體上來講,目前的繼承關係還比較簡單,層次比較淺,也算是一種可以接受的設計思路。如果再繼續加點難度。在剛剛這個場景中,我們只關注「鳥會不會飛」,但如果我們還關注「鳥會不會叫」,那這個時候,我們又該如何設計類之間的繼承關係呢?是否會飛?是否會叫?兩個行為搭配起來會產生四種情況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。如果我們繼續沿用剛才的設計思路,那就需要再定義四個抽象類(abstractflyabletweetablebird、abstractflyableuntweetablebird、abstractunflyabletweetablebird、abstractunflyableuntweetablebird)。

如果我們還需要考慮「是否會下蛋」這樣乙個行為,那估計就要組合**了。類的繼承層次會越來越深、繼承關係會越來越複雜。而這種層次很深、很複雜的繼承關係,一方面,會導致**的可讀性變差。因為我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的**、父類的父類的**……一直追溯到最頂層父類的**。另一方面,這也破壞了類的封裝特性,將父類的實現細節暴露給了子類。子類的實現依賴父類的實現,兩者高度耦合,一旦父類**修改,就會影響所有子類的邏輯。

組合相比繼承有哪些優勢?

實際上,可以利用組合(composition)、介面、委託(delegation)三個技術手段,一塊兒來解決剛剛繼承存在的問題。

介面表示具有某種行為特性。針對「會飛」這樣乙個行為特性,可以定義乙個 flyable 介面,只讓會飛的鳥去實現這個介面。對於會叫、會下蛋這些行為特性,可以類似地定義 tweetable 介面、egglayable 介面。

不過,介面只宣告方法,不定義實現。也就是說,每個會下蛋的鳥都要實現一遍 layegg() 方法,並且實現邏輯是一樣的,這就會導致**重複的問題。那這個問題又該如何解決呢?

繼承主要有三個作用:表示 is-a 關係,支援多型特性,**復用。而這三個作用都可以通過其他技術手段來達成。比如 is-a 關係,可以通過組合和介面的 has-a 關係來替代;多型特性我們可以利用介面來實現;**復用可以通過組合和委託來實現。所以,從理論上講,通過組合、介面、委託三個技術手段,完全可以替換掉繼承,在專案中不用或者少用繼承關係,特別是一些複雜的繼承關係。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。繼承改寫成組合意味著要做更細粒度的類的拆分。這也就意味著,要定義更多的類和介面。類和介面的增多也就或多或少地增加**的複雜程度和維護成本。

所以,在實際的專案開發中,還是要根據具體的情況,來具體選擇該用繼承還是組合。如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不複雜,我們就可以大膽地使用繼承。

反之,系統越不穩定,繼承層次很深,繼承關係複雜,就盡量使用組合來替代繼承。除此之外,還有一些設計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關係,而模板模式(template pattern)使用了繼承關係。

還有一些特殊的場景要求我們必須使用繼承。如果你不能改變乙個函式的入參型別,而入參又非介面,為了支援多型,只能採用繼承來實現。比如下面這樣一段**,其中 feignclient 是乙個外部類,我們沒有許可權去修改這部分**,但是我們希望能重寫這個類在執行時執行的 encode() 函式。這個時候,我們只能採用繼承來實現了。

要杜絕繼承,100% 用組合代替繼承也不對,之所以「多用組合少用繼承」這個口號喊得這麼響,只是因為,長期以來,過度使用繼承。還是那句話,組合並不完美,繼承也不是一無是處。只要控制好它們的***、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合才是正道。

繼承與組合的選擇

1 繼承與組合是重用物件 的機制。2 關於父子類的初始化,請參考初始化列表 不能繼承的函式 l建構函式和析構函式 建構函式與析構函式只負責它所在的那一層上的構建與釋放。子類仍然需要自己的建構函式,也需要手動呼叫父類的自定義建構函式。loperator 理由同上。4 new delete是可以被繼承的...

組合與繼承之間的選擇

下面來看乙個組合的案例 public class engine public void rev public void stop public class wheel public class window public void rolldown public class door public ...

菱形繼承和組合與繼承的選擇。

class person class student public person class teacher public person class assistant public person,public student void test1 由上述的 中可以看出,在給a.name初始化的時候...