在面向對象編程中,有一條很是經典的設計原則,那就是:組合優於繼承,多用組合少用繼承。爲何不推薦使用繼承?組合相比繼承有哪些優點?如何判斷該用組合仍是繼承?今天,咱們就圍繞着這三個問題,來詳細講解一下這條設計原則。編程
在面向對象編程中,有一條很是經典的設計原則,那就是:組合優於繼承,多用組合少用繼承。爲何不推薦使用繼承?組合相比繼承有哪些優點?如何判斷該用組合仍是繼承?今天,咱們就圍繞着這三個問題,來詳細講解一下這條設計原則。設計模式
繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,能夠解決代碼複用的問題。雖然繼承有諸多做用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。因此,對因而否應該在項目中使用繼承,網上有不少爭議。不少人以爲繼承是一種反模式,應該儘可能少用,甚至不用。爲何會有這樣的爭議?咱們經過一個例子來解釋一下。框架
假設咱們要設計一個關於鳥的類。咱們將「鳥類」這樣一個抽象的事物概念,定義爲一個抽象類 AbstractBird。全部更細分的鳥,好比麻雀、鴿子、烏鴉等,都繼承這個抽象類。ide
咱們知道,大部分鳥都會飛,那咱們可不能夠在 AbstractBird 抽象類中,定義一個 fly() 方法呢?答案是否認的。儘管大部分鳥都會飛,但也有特例,好比鴕鳥就不會飛。鴕鳥繼承具備 fly() 方法的父類,那鴕鳥就具備「飛」這樣的行爲,這顯然不符合咱們對現實世界中事物的認識。固然,你可能會說,我在鴕鳥這個子類中重寫(override)fly() 方法,讓它拋出 UnSupportedMethodException 異常不就能夠了嗎?具體的代碼實現以下所示:函數
public class AbstractBird { //...省略其餘屬性和方法... public void fly() { //... } } public class Ostrich extends AbstractBird { //鴕鳥 //...省略其餘屬性和方法... public void fly() { throw new UnSupportedMethodException("I can't fly.'"); } }
這種設計思路雖然能夠解決問題,但不夠優美。由於除了鴕鳥以外,不會飛的鳥還有不少,好比企鵝。對於這些不會飛的鳥來講,咱們都須要重寫 fly() 方法,拋出異常。這樣的設計,一方面,徒增了編碼的工做量;另外一方面,也違背了咱們以後要講的最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不應暴露的接口給外部,增長了類使用過程當中被誤用的機率。this
可能又會說,那咱們再經過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類,不就能夠了嗎?具體的繼承關係以下圖所示:編碼
從圖中咱們能夠看出,繼承關係變成了三層。不過,總體上來說,目前的繼承關係還比較簡單,層次比較淺,也算是一種能夠接受的設計思路。咱們再繼續加點難度。在剛剛這個場景中,咱們只關注「鳥會不會飛」,但若是咱們還關注「鳥會不會叫」,那這個時候,咱們又該如何設計類之間的繼承關係呢?url
是否會飛?是否會叫?兩個行爲搭配起來會產生四種狀況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。若是咱們繼續沿用剛纔的設計思路,那就須要再定義四個抽象類(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。設計
若是咱們還須要考慮「是否會下蛋」這樣一個行爲,那估計就要組合爆炸了。類的繼承層次會愈來愈深、繼承關係會愈來愈複雜。而這種層次很深、很複雜的繼承關係,一方面,會致使代碼的可讀性變差。由於咱們要搞清楚某個類具備哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。另外一方面,這也破壞了類的封裝特性,將父類的實現細節暴露給了子類。子類的實現依賴父類的實現,二者高度耦合,一旦父類代碼修改,就會影響全部子類的邏輯。code
總之,繼承最大的問題就在於:繼承層次過深、繼承關係過於複雜會影響到代碼的可讀性和可維護性。這也是爲何咱們不推薦使用繼承。那剛剛例子中繼承存在的問題,咱們又該如何來解決呢?你能夠先本身思考一下,再聽我下面的講解。
實際上,咱們能夠利用組合(composition)、接口、委託(delegation)三個技術手段,一起來解決剛剛繼承存在的問題。
咱們前面講到接口的時候說過,接口表示具備某種行爲特性。針對「會飛」這樣一個行爲特性,咱們能夠定義一個 Flyable 接口,只讓會飛的鳥去實現這個接口。對於會叫、會下蛋這些行爲特性,咱們能夠相似地定義 Tweetable 接口、EggLayable 接口。
public interface Flyable { void fly(); } public interface Tweetable { void tweet(); } public interface EggLayable { void layEgg(); } public class Ostrich implements Tweetable, EggLayable {//鴕鳥 //... 省略其餘屬性和方法... @Override public void tweet() { //... } @Override public void layEgg() { //... } } public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀 //... 省略其餘屬性和方法... @Override public void fly() { //... } @Override public void tweet() { //... } @Override public void layEgg() { //... } }
不過,咱們知道,接口只聲明方法,不定義實現。也就是說,每一個會下蛋的鳥都要實現一遍 layEgg() 方法,而且實現邏輯是同樣的,這就會致使代碼重複的問題。那這個問題又該如何解決呢?
咱們能夠針對三個接口再定義三個實現類,它們分別是:實現了 fly() 方法的 FlyAbility 類、實現了 tweet() 方法的 TweetAbility 類、實現了 layEgg() 方法的 EggLayAbility 類。而後,經過組合和委託技術來消除代碼重複。具體的代碼實現以下所示:
public interface Flyable { void fly(); } public class FlyAbility implements Flyable { @Override public void fly() { //... } } //省略Tweetable/TweetAbility/EggLayable/EggLayAbility public class Ostrich implements Tweetable, EggLayable {//鴕鳥 private TweetAbility tweetAbility = new TweetAbility(); //組合 private EggLayAbility eggLayAbility = new EggLayAbility(); //組合 //... 省略其餘屬性和方法... @Override public void tweet() { tweetAbility.tweet(); // 委託 } @Override public void layEgg() { eggLayAbility.layEgg(); // 委託 } }
咱們知道繼承主要有三個做用:表示 is-a 關係,支持多態特性,代碼複用。而這三個做用均可以經過其餘技術手段來達成。好比 is-a 關係,咱們能夠經過組合和接口的 has-a 關係來替代;多態特性咱們能夠利用接口來實現;代碼複用咱們能夠經過組合和委託來實現。因此,從理論上講,經過組合、接口、委託三個技術手段,咱們徹底能夠替換掉繼承,在項目中不用或者少用繼承關係,特別是一些複雜的繼承關係。
儘管咱們鼓勵多用組合少用繼承,但組合也並非完美的,繼承也並不是一無可取。從上面的例子來看,繼承改寫成組合意味着要作更細粒度的類的拆分。這也就意味着,咱們要定義更多的類和接口。類和接口的增多也就或多或少地增長代碼的複雜程度和維護成本。因此,在實際的項目開發中,咱們仍是要根據具體的狀況,來具體選擇該用繼承仍是組合。
若是類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(好比,最多有兩層繼承關係),繼承關係不復雜,咱們就能夠大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,咱們就儘可能使用組合來替代繼承。
除此以外,還有一些設計模式會固定使用繼承或者組合。好比,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關係,而模板模式(template pattern)使用了繼承關係。
前面咱們講到繼承能夠實現代碼複用。利用繼承特性,咱們把相同的屬性和方法,抽取出來,定義到父類中。子類複用父類中的屬性和方法,達到代碼複用的目的。可是,有的時候,從業務含義上,A 類和 B 類並不必定具備繼承關係。好比,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但並不具備繼承關係(既不是父子關係,也不是兄弟關係)。僅僅爲了代碼複用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。若是不熟悉背後設計思路的同事,發現 Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的卻只是 URL 相關的操做,會以爲這個代碼寫得莫名其妙,理解不了。這個時候,使用組合就更加合理、更加靈活。具體的代碼實現以下所示:
public class Url { //...省略屬性和方法 } public class Crawler { private Url url; // 組合 public Crawler() { this.url = new Url(); } //... } public class PageAnalyzer { private Url url; // 組合 public PageAnalyzer() { this.url = new Url(); } //.. }
還有一些特殊的場景要求咱們必須使用繼承。若是你不能改變一個函數的入參類型,而入參又非接口,爲了支持多態,只能採用繼承來實現。好比下面這樣一段代碼,其中 FeignClient 是一個外部類,咱們沒有權限去修改這部分代碼,可是咱們但願能重寫這個類在運行時執行的 encode() 函數。這個時候,咱們只能採用繼承來實現了。
public class FeignClient { // feighn client框架代碼 //...省略其餘代碼... public void encode(String url) { //... } } public void demofunction(FeignClient feignClient) { //... feignClient.encode(url); //... } public class CustomizedFeignClient extends FeignClient { @Override public void encode(String url) { //...重寫encode的實現...} } // 調用 FeignClient client = new CustomizedFeignClient(); demofunction(client);
儘管有些人說,要杜絕繼承,100% 用組合代替繼承,可是個人觀點沒那麼極端!之因此「多用組合少用繼承」這個口號喊得這麼響,只是由於,長期以來,咱們過分使用繼承。仍是那句話,組合並不完美,繼承也不是一無可取。只要咱們控制好它們的反作用、發揮它們各自的優點,在不一樣的場合下,恰當地選擇使用繼承仍是組合,這纔是咱們所追求的境界。
繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,能夠解決代碼複用的問題。雖然繼承有諸多做用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種狀況下,咱們應該儘可能少用,甚至不用繼承。
繼承主要有三個做用:表示 is-a 關係,支持多態特性,代碼複用。而這三個做用均可以經過組合、接口、委託三個技術手段來達成。除此以外,利用組合還能解決層次過深、過複雜的繼承關係影響代碼可維護性的問題。
儘管咱們鼓勵多用組合少用繼承,但組合也並非完美的,繼承也並不是一無可取。在實際的項目開發中,咱們仍是要根據具體的狀況,來選擇該用繼承仍是組合。若是類之間的繼承結構穩定,層次比較淺,關係不復雜,咱們就能夠大膽地使用繼承。反之,咱們就儘可能使用組合來替代繼承。除此以外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。