<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>

          面試官:Java 設(shè)計(jì)原則中,為什么反復(fù)強(qiáng)調(diào)組合要優(yōu)先于繼承?

          共 5785字,需瀏覽 12分鐘

           ·

          2022-03-10 08:52

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          來(lái)源:blog.csdn.net/fuzhongmin05/article/details/108646872

          作者:GeorgiaStar

          面向?qū)ο缶幊讨?,有一條非常經(jīng)典的設(shè)計(jì)原則,那就是:組合優(yōu)于繼承,多用組合少用繼承。


          同樣地,在《阿里巴巴Java開(kāi)發(fā)手冊(cè)》中有一條規(guī)定:謹(jǐn)慎使用繼承的方式進(jìn)行擴(kuò)展,優(yōu)先使用組合的方式實(shí)現(xiàn)。?



          為什么不推薦使用繼承


          每個(gè)人在剛剛學(xué)習(xí)面向?qū)ο缶幊虝r(shí)都會(huì)覺(jué)得:繼承可以實(shí)現(xiàn)類的復(fù)用。所以,很多開(kāi)發(fā)人員在需要復(fù)用一些代碼的時(shí)候會(huì)很自然的使用類的繼承的方式,因?yàn)闀?shū)上就是這么寫(xiě)的。繼承是面向?qū)ο蟮乃拇筇匦灾唬脕?lái)表示類之間的is-a關(guān)系,可以解決代碼復(fù)用的問(wèn)題。雖然繼承有諸多作用,但繼承層次過(guò)深、過(guò)復(fù)雜,也會(huì)影響到代碼的可維護(hù)性。


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


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


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


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


          這種寫(xiě)法雖然可以解決問(wèn)題,但不優(yōu)雅。因?yàn)槌锁r鳥(niǎo)之外,不會(huì)飛的鳥(niǎo)還有很多,比如企鵝。對(duì)于這些不會(huì)飛的鳥(niǎo)來(lái)說(shuō),全部都去重寫(xiě)fly()方法,拋出異常,完全屬于代碼重復(fù)。理論上這些不會(huì)飛的鳥(niǎo)根本就不應(yīng)該擁有fly()方法,讓不會(huì)飛的鳥(niǎo)暴露fly()接口給外部,增加了被誤用的概率。


          要解決上面的問(wèn)題,就得讓AbstractBird類派生出兩個(gè)更加細(xì)分的抽象類:會(huì)飛的鳥(niǎo)類AbstractFlyableBird和不會(huì)飛的鳥(niǎo)類AbstractUnFlyableBird,讓麻雀、烏鴉這些會(huì)飛的鳥(niǎo)都繼承 AbstractFlyableBird,讓鴕鳥(niǎo)、企鵝這些不會(huì)飛的鳥(niǎo),都繼承 AbstractUnFlyableBird 類。


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



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



          如果繼續(xù)增加“鳥(niǎo)會(huì)不會(huì)下蛋”這樣的行為,類的繼承層次會(huì)越來(lái)越深、繼承關(guān)系會(huì)越來(lái)越復(fù)雜。而這種層次很深、很復(fù)雜的繼承關(guān)系,一方面,會(huì)導(dǎo)致代碼的可讀性變差。


          因?yàn)槲覀円闱宄硞€(gè)類具有哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。另一方面,這也破壞了類的封裝特性,將父類的實(shí)現(xiàn)細(xì)節(jié)暴露給了子類。子類的實(shí)現(xiàn)依賴父類的實(shí)現(xiàn),兩者高度耦合,一旦父類代碼修改,就會(huì)影響所有子類的邏輯。


          繼承最大的問(wèn)題就在于:繼承層次過(guò)深、繼承關(guān)系過(guò)于復(fù)雜時(shí)會(huì)影響到代碼的可讀性和可維護(hù)性。


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


          復(fù)用性是面向?qū)ο蠹夹g(shù)帶來(lái)的很棒的潛在好處之一。如果運(yùn)用的好的話可以幫助我們節(jié)省很多開(kāi)發(fā)時(shí)間,提升開(kāi)發(fā)效率。但是,如果被濫用那么就可能產(chǎn)生很多難以維護(hù)的代碼。作為一門面向?qū)ο箝_(kāi)發(fā)的語(yǔ)言,代碼復(fù)用是Java引人注意的功能之一。


          Java代碼的復(fù)用有繼承、組合以及委托三種具體的實(shí)現(xiàn)形式。


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


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


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



          不過(guò),接口只聲明方法,不定義實(shí)現(xiàn)。也就是說(shuō),每個(gè)會(huì)下蛋的鳥(niǎo)都要實(shí)現(xiàn)一遍layEgg()方法,并且實(shí)現(xiàn)邏輯幾乎是一樣的(可能極少場(chǎng)景下會(huì)不一樣),這就會(huì)導(dǎo)致代碼重復(fù)的問(wèn)題。那這個(gè)問(wèn)題又該如何解決呢?有以下兩種方法。


          使用委托


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


          public interface Flyable {  void fly();}public class FlyAbility implements Flyable {  @Override  public void fly() { //... }}//省略Tweetable/TweetAbility/EggLayable/EggLayAbilitypublic class Ostrich implements Tweetable, EggLayable {//鴕鳥(niǎo)  private TweetAbility tweetAbility = new TweetAbility(); //組合  private EggLayAbility eggLayAbility = new EggLayAbility(); //組合  //... 省略其他屬性和方法...  @Override  public void tweet() {    tweetAbility.tweet(); // 委托  }  @Override  public void layEgg() {    eggLayAbility.layEgg(); // 委托  }}


          使用Java8的接口默認(rèn)方法


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


          public interface Flyable {  default void fly() {    //默認(rèn)實(shí)現(xiàn)...  }}
          public interface Flyable { default void fly() { //默認(rèn)實(shí)現(xiàn)... }}
          public interface Tweetable { default void tweet() { //默認(rèn)實(shí)現(xiàn)... }}
          public interface EggLayable { default void layEgg() { //默認(rèn)實(shí)現(xiàn)... }}
          public class Ostrich implements Tweetable, EggLayable {//鴕鳥(niǎo) //... 省略其他屬性和方法...}public class Sparrow implements Flayable, Tweetable, EggLayable {//麻雀 //... 省略其他屬性和方法...}


          繼承主要有三個(gè)作用:表示is-a關(guān)系、支持多態(tài)特性、代碼復(fù)用。而這三個(gè)作用都可以通過(guò)其他技術(shù)手段來(lái)達(dá)成。比如is-a關(guān)系,我們可以通過(guò)組合和接口的has-a關(guān)系來(lái)替代;多態(tài)特性我們也可以利用接口來(lái)實(shí)現(xiàn);代碼復(fù)用我們可以通過(guò)組合和委托來(lái)實(shí)現(xiàn)。


          所以,從理論上講,通過(guò)組合、接口、委托三個(gè)技術(shù)手段,我們完全可以替換掉繼承,在項(xiàng)目中不用或者少用繼承關(guān)系,特別是一些復(fù)雜的繼承關(guān)系。


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


          盡管我們鼓勵(lì)多用組合少用繼承,但組合也并不是完美的,繼承也并非一無(wú)是處。從上面的例子來(lái)看,繼承改寫(xiě)成組合意味著要做更細(xì)粒度的類的拆分。這也就意味著,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的復(fù)雜程度和維護(hù)成本。


          如果類之間的繼承結(jié)構(gòu)穩(wěn)定(不會(huì)輕易改變),繼承層次比較淺(比如,最多有兩層繼承關(guān)系),繼承關(guān)系不復(fù)雜,我們就可以大膽地使用繼承。反之,系統(tǒng)越不穩(wěn)定,繼承層次很深,繼承關(guān)系復(fù)雜,我們就盡量使用組合來(lái)替代繼承。


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


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


          比如你要寫(xiě)一個(gè)畫(huà)圖系統(tǒng),畫(huà)不同的圖形,這個(gè)時(shí)候,你可能考慮到調(diào)用相應(yīng)的函數(shù)的時(shí)候可以不考慮具體類型,直接畫(huà)就好了,具體什么圖形,交給運(yùn)行時(shí)去判斷。這個(gè)時(shí)候,就要用到多態(tài),就需要有繼承關(guān)系。一個(gè)父類,多個(gè)子類。然后用父類的類型去引用具體子類的對(duì)象,就可以了。


          而用不到多態(tài)的時(shí)候,使用繼承有什么用呢?代碼復(fù)用?一個(gè)繼承可以讓你少寫(xiě)很多代碼,但是用錯(cuò)了場(chǎng)合,后期的維護(hù)可能是災(zāi)難性的。因?yàn)槔^承關(guān)系的耦合度很高,一處改會(huì)導(dǎo)致處處需要修改。這個(gè)時(shí)候就需要組合。


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


          處境尷尬的繼承



          大家對(duì)繼承的厭惡主要是因?yàn)殚L(zhǎng)期以來(lái)程序員過(guò)度使用繼承,繼承并非一無(wú)是處。


          在某些特殊場(chǎng)景下,我們必須使用繼承。如果你不能改變一個(gè)函數(shù)的入?yún)㈩愋?,而入?yún)⒂址墙涌?,為了支持多態(tài),只能采用繼承來(lái)實(shí)現(xiàn)。比如下面這樣一段代碼,其中FeignClient是一個(gè)外部類,我們無(wú)法修改這個(gè)外部類,但是我們希望能重寫(xiě)這個(gè)類在運(yùn)行時(shí)執(zhí)行的encode() 函數(shù)。這個(gè)時(shí)候,我們只能采用繼承來(lái)實(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) { //...重寫(xiě)encode的實(shí)現(xiàn)...}}// 調(diào)用FeignClient client = new CustomizedFeignClient();demofunction(client);


          上面這個(gè)例子,舉得不是太恰當(dāng),更像是一種迫不得已。這恰好反映了繼承在面向?qū)ο缶幊痰拇蟛糠謭?chǎng)景下的尷尬處境。


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


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

          ????

          1、來(lái)開(kāi)發(fā)SQL,沒(méi)

          2、?Chrome,會(huì)沒(méi)個(gè)?

          3、個(gè)SpringBoot44Java

          4、QQ個(gè),!

          5、SpringBoot?開(kāi)發(fā)過(guò)?

          點(diǎn)

          點(diǎn)

          點(diǎn)點(diǎn)

          點(diǎn)

          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  人人搞人人摸 | 日韩欧美18禁 | 大香蕉伊人免费在线 | 欧美日韩黄色片在线看 | 人人操人人摸人人透 |