<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          在設(shè)計原則中,為什么反復強調(diào)組合要優(yōu)于繼承?

          共 7521字,需瀏覽 16分鐘

           ·

          2021-08-21 19:31

          今日推薦
          21 款 yyds 的 IDEA插件
          這 56 個代碼注釋讓我笑吐了
          注解+反射優(yōu)雅的實現(xiàn)Excel導入導出(通用版)
          Fluent Mybatis 牛逼!
          Nginx 常用配置清單
          這玩意比ThreadLocal叼多了,嚇得我趕緊分享出來。

          面向?qū)ο缶幊讨校幸粭l非常經(jīng)典的設(shè)計原則,那就是:組合優(yōu)于繼承,多用組合少用繼承。同樣地,在《阿里巴巴Java開發(fā)手冊》中有一條規(guī)定:謹慎使用繼承的方式進行擴展,優(yōu)先使用組合的方式實現(xiàn)。

          為什么不推薦使用繼承

          每個人在剛剛學習面向?qū)ο缶幊虝r都會覺得:繼承可以實現(xiàn)類的復用。所以,很多開發(fā)人員在需要復用一些代碼的時候會很自然的使用類的繼承的方式,因為書上就是這么寫的。繼承是面向?qū)ο蟮乃拇筇匦灾?,用來表示類之間的is-a關(guān)系,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過復雜,也會影響到代碼的可維護性。

          假設(shè)我們要設(shè)計一個關(guān)于鳥的類。我們將“鳥”這樣一個抽象的事物概念,定義為一個抽象類AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。我們知道,大部分鳥都會飛,那我們可不可以在 AbstractBird抽象類中,定義一個fly()方法呢?

          答案是否定的。盡管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有fly()方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不對。如果在鴕鳥這個子類中重寫fly() 方法,讓它拋出UnSupportedMethodException異常呢?

          具體的代碼實現(xiàn)如下所示:

          public class AbstractBird {
            //...省略其他屬性和方法...
            public void fly() //... }
          }

          public class Ostrich extends AbstractBird //鴕鳥
            //...省略其他屬性和方法...
            public void fly() {
              throw new UnSupportedMethodException("I can't fly.'");
            }
          }

          這種寫法雖然可以解決問題,但不優(yōu)雅。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對于這些不會飛的鳥來說,全部都去重寫fly()方法,拋出異常,完全屬于代碼重復。理論上這些不會飛的鳥根本就不應該擁有fly()方法,讓不會飛的鳥暴露fly()接口給外部,增加了被誤用的概率。

          要解決上面的問題,就得讓AbstractBird類派生出兩個更加細分的抽象類:會飛的鳥類AbstractFlyableBird和不會飛的鳥類AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類。往期面試題匯總:250期面試資料

          具體的繼承關(guān)系如下圖所示:

          這樣一來,繼承關(guān)系變成了三層。但是如果我們不只關(guān)注“鳥會不會飛”,還要繼續(xù)關(guān)注“鳥會不會叫”,將鳥劃分得更加細致時呢?兩個關(guān)注行為自由搭配起來會產(chǎn)生四種情況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。如果繼續(xù)沿用剛才的設(shè)計思路,繼承層次會再次加深。

          如果繼續(xù)增加“鳥會不會下蛋”這樣的行為,類的繼承層次會越來越深、繼承關(guān)系會越來越復雜。而這種層次很深、很復雜的繼承關(guān)系,一方面,會導致代碼的可讀性變差。因為我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。另一方面,這也破壞了類的封裝特性,將父類的實現(xiàn)細節(jié)暴露給了子類。子類的實現(xiàn)依賴父類的實現(xiàn),兩者高度耦合,一旦父類代碼修改,就會影響所有子類的邏輯。

          繼承最大的問題就在于:繼承層次過深、繼承關(guān)系過于復雜時會影響到代碼的可讀性和可維護性。

          組合相比繼承有哪些優(yōu)勢

          復用性是面向?qū)ο蠹夹g(shù)帶來的很棒的潛在好處之一。如果運用的好的話可以幫助我們節(jié)省很多開發(fā)時間,提升開發(fā)效率。但是,如果被濫用那么就可能產(chǎn)生很多難以維護的代碼。作為一門面向?qū)ο箝_發(fā)的語言,代碼復用是Java引人注意的功能之一。Java代碼的復用有繼承、組合以及委托三種具體的實現(xiàn)形式。

          對于上面提到的繼承帶來的問題,可以利用組合(composition)、接口、委托(delegation)三個技術(shù)手段一塊兒來解決。

          接口表示具有某種行為特性。針對“會飛”這樣一個行為特性,我們可以定義一個Flyable接口,只讓會飛的鳥去實現(xiàn)這個接口。對于會叫、會下蛋這些行為特性,我們可以類似地定義Tweetable接口、EggLayable接口。我們將這個設(shè)計思路翻譯成Java代碼的話,就是下面這個樣子:

          public interface Flyable {
            void fly();
          }
          public interface Tweetable {
            void tweet();
          }
          public interface EggLayable {
            void layEgg();
          }
          public class Ostrich implements TweetableEggLayable {//鴕鳥
            //... 省略其他屬性和方法...
            @Override
            public void tweet() //... }
            @Override
            public void layEgg() //... }
          }
          public class Sparrow implements FlayableTweetableEggLayable {//麻雀
            //... 省略其他屬性和方法...
            @Override
            public void fly() //... }
            @Override
            public void tweet() //... }
            @Override
            public void layEgg() //... }
          }

          不過,接口只聲明方法,不定義實現(xiàn)。也就是說,每個會下蛋的鳥都要實現(xiàn)一遍layEgg()方法,并且實現(xiàn)邏輯幾乎是一樣的(可能極少場景下會不一樣),這就會導致代碼重復的問題。那這個問題又該如何解決呢?有以下兩種方法。往期面試題匯總:250期面試資料

          使用委托

          針對三個接口再定義三個實現(xiàn)類,它們分別是:實現(xiàn)了fly()方法的 FlyAbility類、實現(xiàn)了tweet()方法的TweetAbility類、實現(xiàn)了layEgg()方法的 EggLayAbility類。然后,通過組合和委托技術(shù)來消除代碼重復。

          public interface Flyable {
            void fly();
          }
          public class FlyAbility implements Flyable 
          {
            @Override
            public void fly() //... }
          }
          //省略Tweetable/TweetAbility/EggLayable/EggLayAbility

          public class Ostrich implements TweetableEggLayable {//鴕鳥
            private TweetAbility tweetAbility = new TweetAbility(); //組合
            private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
            //... 省略其他屬性和方法...
            @Override
            public void tweet() {
              tweetAbility.tweet(); // 委托
            }
            @Override
            public void layEgg() {
              eggLayAbility.layEgg(); // 委托
            }
          }

          使用Java8的接口默認方法

          在Java8中,我們可以在接口中寫默認實現(xiàn)方法。使用關(guān)鍵字default定義默認接口實現(xiàn),當然這個默認的方法也可以重寫。

          public interface Flyable {
            default void fly() {
              //默認實現(xiàn)... 
            }
          }


          public interface Flyable {
            default void fly() {
              //默認實現(xiàn)... 
            }
          }

          public interface Tweetable {
            default void tweet() {
              //默認實現(xiàn)... 
            }
          }

          public interface EggLayable {
            default void layEgg() {
              //默認實現(xiàn)... 
            }
          }

          public class Ostrich implements TweetableEggLayable {//鴕鳥
            //... 省略其他屬性和方法...
          }
          public class Sparrow implements FlayableTweetableEggLayable {//麻雀
            //... 省略其他屬性和方法...
          }

          繼承主要有三個作用:表示is-a關(guān)系、支持多態(tài)特性、代碼復用。而這三個作用都可以通過其他技術(shù)手段來達成。比如is-a關(guān)系,我們可以通過組合和接口的has-a關(guān)系來替代;多態(tài)特性我們也可以利用接口來實現(xiàn);代碼復用我們可以通過組合和委托來實現(xiàn)。所以,從理論上講,通過組合、接口、委托三個技術(shù)手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關(guān)系,特別是一些復雜的繼承關(guān)系。

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

          盡管我們鼓勵多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。從上面的例子來看,繼承改寫成組合意味著要做更細粒度的類的拆分。這也就意味著,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的復雜程度和維護成本。如果類之間的繼承結(jié)構(gòu)穩(wěn)定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關(guān)系),繼承關(guān)系不復雜,我們就可以大膽地使用繼承。反之,系統(tǒng)越不穩(wěn)定,繼承層次很深,繼承關(guān)系復雜,我們就盡量使用組合來替代繼承。

          除此之外,還有一些設(shè)計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關(guān)系,而模板模式(template pattern)使用了繼承關(guān)系。

          有的地方提到組合優(yōu)先繼承這條軟件開發(fā)原則時,可能會說成“多用組合,少用繼承”。所謂多用與少用,實際指的是要弄清楚在具體的場景下需要哪種。軟件開發(fā)原則這類問題,不宜死扣字眼。其實在《Thinking in Java》里有提到,當你用繼承的時候,肯定是想要使用多態(tài)的特性。

          比如你要寫一個畫圖系統(tǒng),畫不同的圖形,這個時候,你可能考慮到調(diào)用相應的函數(shù)的時候可以不考慮具體類型,直接畫就好了,具體什么圖形,交給運行時去判斷。這個時候,就要用到多態(tài),就需要有繼承關(guān)系。一個父類,多個子類。然后用父類的類型去引用具體子類的對象,就可以了。往期面試題匯總:250期面試資料

          而用不到多態(tài)的時候,使用繼承有什么用呢?代碼復用?一個繼承可以讓你少寫很多代碼,但是用錯了場合,后期的維護可能是災難性的。因為繼承關(guān)系的耦合度很高,一處改會導致處處需要修改。這個時候就需要組合。

          所以我堅持,如果不想使用多態(tài)特性,繼承關(guān)系就是無用的。

          處境尷尬的繼承

          大家對繼承的厭惡主要是因為長期以來程序員過度使用繼承,繼承并非一無是處。

          在某些特殊場景下,我們必須使用繼承。如果你不能改變一個函數(shù)的入?yún)㈩愋?,而入?yún)⒂址墙涌?,為了支持多態(tài),只能采用繼承來實現(xiàn)。比如下面這樣一段代碼,其中FeignClient是一個外部類,我們無法修改這個外部類,但是我們希望能重寫這個類在運行時執(zhí)行的encode() 函數(shù)。這個時候,我們只能采用繼承來實現(xiàn)了。

          public class FeignClient // Feign 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的實現(xiàn)...}
          }

          // 調(diào)用
          FeignClient client = new CustomizedFeignClient();
          demofunction(client);

          上面這個例子,舉得不是太恰當,更像是一種迫不得已。這恰好反映了繼承在面向?qū)ο缶幊痰拇蟛糠謭鼍跋碌膶擂翁幘场?/p>

          其實我們很難真正使用好繼承,根本原因在于,自然界中,代際之間是存在變異的,物種之間也是,而且這種變化是無法做規(guī)律化描述的,既伴隨著某些功能的增加,也伴隨著某些功能的弱化,甚至還有某些功能的改變。

          在軟件行業(yè)最早期,軟件功能很貧乏,需要不斷增加軟件功能來滿足需求,這時候繼承關(guān)系能夠體現(xiàn)軟件迭代后功能增強的特點。但很快就達到瓶頸期,功能不再是衡量軟件好壞的主要指標,各種差異化的體驗變得更加重要,此時軟件迭代時不再是單純的功能的累加,甚至于是完全的推倒重來,編程語言上的繼承關(guān)系也就隨之被廢棄。

          注:以上關(guān)于組合及繼承的代碼例子,出自極客時間王爭老師的《設(shè)計模式之美》第十講

          感謝閱讀,希望對你有所幫助 :) 

          來源:blog.csdn.net/fuzhongmin05/article/details/108646872

          更多推薦


          1、30多個Java實戰(zhàn)項目,全部在這里了
          2、「吐血」我把大學4年、研究生3年的全部學習資源都分享在這里了
          3、一個基于spring+springmvc+mybatis的高仿bilibili的項目...

          瀏覽 35
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  A性视频| 国产高清无码在线不卡视频 | 五月丁香中文字幕成人网在线 | 黄色一级播放 | 一级AA免费播放 |