国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

設(shè)計模式-01.設(shè)計思想

這篇具有很好參考價值的文章主要介紹了設(shè)計模式-01.設(shè)計思想。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

設(shè)計思想

  1. 此系列文章非本人原創(chuàng),是學習筆記。

下面講一些常見的設(shè)計思想

基于接口而非實現(xiàn)編程

這個原則非常重要,是一種非常有效的提高代碼質(zhì)量的手段,在平時的開發(fā)中特別經(jīng)常被用到。

如何解讀原則中的“接口”二字?

  1. “基于接口而非實現(xiàn)編程”這條原則的英文描述是:“Program to an interface, not an implementation”。我們理解這條原則的時候,千萬不要一開始就與具體的編程語言掛鉤,局限在編程語言的“接口”語法中(比如 Java 中的 interface 接口語法)。這條原則最早出現(xiàn)于 1994 年 GoF 的《設(shè)計模式》這本書,它先于很多編程語言而誕生(比如 Java 語言),是一條比較抽象、泛化的設(shè)計思想。
  2. 實際上,理解這條原則的關(guān)鍵,就是理解其中的“接口”兩個字。還記得我們上一節(jié)課講的“接口”的定義嗎?從本質(zhì)上來看,“接口”就是一組“協(xié)議”或者“約定”,是功能提供者提供給使用者的一個“功能列表”?!敖涌凇痹诓煌膽脠鼍跋聲胁煌慕庾x,比如服務端與客戶端之間的“接口”,類庫提供的“接口”,甚至是一組通信的協(xié)議都可以叫作“接口”。剛剛對“接口”的理解,都比較偏上層、偏抽象,與實際的寫代碼離得有點遠。如果落實到具體的編碼,“基于接口而非實現(xiàn)編程”這條原則中的“接口”,可以理解為編程語言中的接口或者抽象類。
  3. 前面我們提到,這條原則能非常有效地提高代碼質(zhì)量,之所以這么說,那是因為,應用這條原則,可以將接口和實現(xiàn)相分離,封裝不穩(wěn)定的實現(xiàn),暴露穩(wěn)定的接口。上游系統(tǒng)面向接口而非實現(xiàn)編程,不依賴不穩(wěn)定的實現(xiàn)細節(jié),這樣當實現(xiàn)發(fā)生變化的時候,上游系統(tǒng)的代碼基本上不需要做改動,以此來降低耦合性,提高擴展性。
  4. 實際上,“基于接口而非實現(xiàn)編程”這條原則的另一個表述方式,是“基于抽象而非實現(xiàn)編程”。后者的表述方式其實更能體現(xiàn)這條原則的設(shè)計初衷。在軟件開發(fā)中,最大的挑戰(zhàn)之一就是需求的不斷變化,這也是考驗代碼設(shè)計好壞的一個標準。越抽象、越頂層、越脫離具體某一實現(xiàn)的設(shè)計,越能提高代碼的靈活性,越能應對未來的需求變化。好的代碼設(shè)計,不僅能應對當下的需求,而且在將來需求發(fā)生變化的時候,仍然能夠在不破壞原有代碼設(shè)計的情況下靈活應對。而抽象就是提高代碼擴展性、靈活性、可維護性最有效的手段之一。

如何將這條原則應用到實戰(zhàn)中?

假設(shè)我們的系統(tǒng)中有很多涉及圖片處理和存儲的業(yè)務邏輯。圖片經(jīng)過處理之后被上傳到阿里云上。為了代碼復用,我們封裝了圖片存儲相關(guān)的代碼邏輯,提供了一個統(tǒng)一的 AliyunImageStore 類,供整個系統(tǒng)來使用。具體的代碼實現(xiàn)如下所示:

public class AliyunImageStore {
  //...省略屬性、構(gòu)造函數(shù)等...
  
  public void createBucketIfNotExisting(String bucketName) {
    // ...創(chuàng)建bucket代碼邏輯...
    // ...失敗會拋出異常..
  }
  
  public String generateAccessToken() {
    // ...根據(jù)accesskey/secrectkey等生成access token
  }
  
  public String uploadToAliyun(Image image, String bucketName, String accessToken) {
    //...上傳圖片到阿里云...
    //...返回圖片存儲在阿里云上的地址(url)...
  }
  
  public Image downloadFromAliyun(String url, String accessToken) {
    //...從阿里云下載圖片...
  }
}

// AliyunImageStore類的使用舉例
public class ImageProcessingJob {
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他無關(guān)代碼...
  
  public void process() {
    Image image = ...; //處理圖片,并封裝為Image對象
    AliyunImageStore imageStore = new AliyunImageStore(/*省略參數(shù)*/);
    imageStore.createBucketIfNotExisting(BUCKET_NAME);
    String accessToken = imageStore.generateAccessToken();
    imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);
  }
  
}
  1. 整個上傳流程包含三個步驟:創(chuàng)建 bucket(你可以簡單理解為存儲目錄)、生成 access token 訪問憑證、攜帶 access token 上傳圖片到指定的 bucket 中。代碼實現(xiàn)非常簡單,類中的幾個方法定義得都很干凈,用起來也很清晰,乍看起來沒有太大問題,完全能滿足我們將圖片存儲在阿里云的業(yè)務需求。
  2. 不過,軟件開發(fā)中唯一不變的就是變化。過了一段時間后,我們自建了私有云,不再將圖片存儲到阿里云了,而是將圖片存儲到自建私有云上。為了滿足這樣一個需求的變化,我們該如何修改代碼呢?
  3. 我們需要重新設(shè)計實現(xiàn)一個存儲圖片到私有云的 PrivateImageStore 類,并用它替換掉項目中所有的 AliyunImageStore 類對象。這樣的修改聽起來并不復雜,只是簡單替換而已,對整個代碼的改動并不大。不過,我們經(jīng)常說,“細節(jié)是魔鬼”。這句話在軟件開發(fā)中特別適用。實際上,剛剛的設(shè)計實現(xiàn)方式,就隱藏了很多容易出問題的“魔鬼細節(jié)”,我們一塊來看看都有哪些。
  4. 新的 PrivateImageStore 類需要設(shè)計實現(xiàn)哪些方法,才能在盡量最小化代碼修改的情況下,替換掉 AliyunImageStore 類呢?這就要求我們必須將 AliyunImageStore 類中所定義的所有 public 方法,在 PrivateImageStore 類中都逐一定義并重新實現(xiàn)一遍。而這樣做就會存在一些問題,我總結(jié)了下面兩點。
    • 首先,AliyunImageStore 類中有些函數(shù)命名暴露了實現(xiàn)細節(jié),比如,uploadToAliyun() 和 downloadFromAliyun()。如果開發(fā)這個功能的同事沒有接口意識、抽象思維,那這種暴露實現(xiàn)細節(jié)的命名方式就不足為奇了,畢竟最初我們只考慮將圖片存儲在阿里云上。而我們把這種包含“aliyun”字眼的方法,照抄到 PrivateImageStore 類中,顯然是不合適的。如果我們在新類中重新命名 uploadToAliyun()、downloadFromAliyun() 這些方法,那就意味著,我們要修改項目中所有使用到這兩個方法的代碼,代碼修改量可能就會很大。
    • 其次,將圖片存儲到阿里云的流程,跟存儲到私有云的流程,可能并不是完全一致的。比如,阿里云的圖片上傳和下載的過程中,需要生產(chǎn) access token,而私有云不需要 access token。一方面,AliyunImageStore 中定義的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另一方面,我們在使用 AliyunImageStore 上傳、下載圖片的時候,代碼中用到了 generateAccessToken() 方法,如果要改為私有云的上傳下載流程,這些代碼都需要做調(diào)整。
  5. 那這兩個問題該如何解決呢?解決這個問題的根本方法就是,在編寫代碼的時候,要遵從“基于接口而非實現(xiàn)編程”的原則,具體來講,我們需要做到下面這 3 點。
    1. 函數(shù)的命名不能暴露任何實現(xiàn)細節(jié)。比如,前面提到的 uploadToAliyun() 就不符合要求,應該改為去掉 aliyun 這樣的字眼,改為更加抽象的命名方式,比如:upload()。
    2. 封裝具體的實現(xiàn)細節(jié)。比如,跟阿里云相關(guān)的特殊上傳(或下載)流程不應該暴露給調(diào)用者。我們對上傳(或下載)流程進行封裝,對外提供一個包裹所有上傳(或下載)細節(jié)的方法,給調(diào)用者使用。
    3. 為實現(xiàn)類定義抽象的接口。具體的實現(xiàn)類都依賴統(tǒng)一的接口定義,遵從一致的上傳功能協(xié)議。使用者依賴接口,而不是具體的實現(xiàn)類來編程。
  6. 我們按照這個思路,把代碼重構(gòu)一下。重構(gòu)后的代碼如下所示:
public interface ImageStore {
  String upload(Image image, String bucketName);
  Image download(String url);
}

public class AliyunImageStore implements ImageStore {
  //...省略屬性、構(gòu)造函數(shù)等...

  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
    String accessToken = generateAccessToken();
    //...上傳圖片到阿里云...
    //...返回圖片在阿里云上的地址(url)...
  }

  public Image download(String url) {
    String accessToken = generateAccessToken();
    //...從阿里云下載圖片...
  }

  private void createBucketIfNotExisting(String bucketName) {
    // ...創(chuàng)建bucket...
    // ...失敗會拋出異常..
  }

  private String generateAccessToken() {
    // ...根據(jù)accesskey/secrectkey等生成access token
  }
}

// 上傳下載流程改變:私有云不需要支持access token
public class PrivateImageStore implements ImageStore  {
  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
    //...上傳圖片到私有云...
    //...返回圖片的url...
  }

  public Image download(String url) {
    //...從私有云下載圖片...
  }

  private void createBucketIfNotExisting(String bucketName) {
    // ...創(chuàng)建bucket...
    // ...失敗會拋出異常..
  }
}

// ImageStore的使用舉例
public class ImageProcessingJob {
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他無關(guān)代碼...
  
  public void process() {
    Image image = ...;//處理圖片,并封裝為Image對象
    ImageStore imageStore = new PrivateImageStore(...);
    imagestore.upload(image, BUCKET_NAME);
  }
}
  1. 除此之外,很多人在定義接口的時候,希望通過實現(xiàn)類來反推接口的定義。先把實現(xiàn)類寫好,然后看實現(xiàn)類中有哪些方法,照抄到接口定義中。如果按照這種思考方式,就有可能導致接口定義不夠抽象,依賴具體的實現(xiàn)。這樣的接口設(shè)計就沒有意義了。不過,如果你覺得這種思考方式更加順暢,那也沒問題,只是將實現(xiàn)類的方法搬移到接口定義中的時候,要有選擇性的搬移,不要將跟具體實現(xiàn)相關(guān)的方法搬移到接口中,比如 AliyunImageStore 中的 generateAccessToken() 方法。
  2. 總結(jié)一下,我們在做軟件開發(fā)的時候,一定要有抽象意識、封裝意識、接口意識。在定義接口的時候,不要暴露任何實現(xiàn)細節(jié)。接口的定義只表明做什么,而不是怎么做。而且,在設(shè)計接口的時候,我們要多思考一下,這樣的接口設(shè)計是否足夠通用,是否能夠做到在替換具體的接口實現(xiàn)的時候,不需要任何接口定義的改動。

是否需要為每個類定義接口?

  1. 看了剛剛的講解,你可能會有這樣的疑問:為了滿足這條原則,我是不是需要給每個實現(xiàn)類都定義對應的接口呢?在開發(fā)的時候,是不是任何代碼都要只依賴接口,完全不依賴實現(xiàn)編程呢?
  2. 做任何事情都要講求一個“度”,過度使用這條原則,非得給每個類都定義接口,接口滿天飛,也會導致不必要的開發(fā)負擔。至于什么時候,該為某個類定義接口,實現(xiàn)基于接口的編程,什么時候不需要定義接口,直接使用實現(xiàn)類編程,我們做權(quán)衡的根本依據(jù),還是要回歸到設(shè)計原則誕生的初衷上來。只要搞清楚了這條原則是為了解決什么樣的問題而產(chǎn)生的,你就會發(fā)現(xiàn),很多之前模棱兩可的問題,都會變得豁然開朗。
  3. 前面我們也提到,這條原則的設(shè)計初衷是,將接口和實現(xiàn)相分離,封裝不穩(wěn)定的實現(xiàn),暴露穩(wěn)定的接口。上游系統(tǒng)面向接口而非實現(xiàn)編程,不依賴不穩(wěn)定的實現(xiàn)細節(jié),這樣當實現(xiàn)發(fā)生變化的時候,上游系統(tǒng)的代碼基本上不需要做改動,以此來降低代碼間的耦合性,提高代碼的擴展性。
  4. 從這個設(shè)計初衷上來看,如果在我們的業(yè)務場景中,某個功能只有一種實現(xiàn)方式,未來也不可能被其他實現(xiàn)方式替換,那我們就沒有必要為其設(shè)計接口,也沒有必要基于接口編程,直接使用實現(xiàn)類就可以了。
  5. 除此之外,越是不穩(wěn)定的系統(tǒng),我們越是要在代碼的擴展性、維護性上下功夫。相反,如果某個系統(tǒng)特別穩(wěn)定,在開發(fā)完之后,基本上不需要做維護,那我們就沒有必要為其擴展性,投入不必要的開發(fā)時間。

多用組合少用繼承

在面向?qū)ο缶幊讨?,有一條非常經(jīng)典的設(shè)計原則,那就是:組合優(yōu)于繼承,多用組合少用繼承。為什么不推薦使用繼承?組合相比繼承有哪些優(yōu)勢?如何判斷該用組合還是繼承?今天,我們就圍繞著這三個問題,來詳細講解一下這條設(shè)計原則。

為什么不推薦使用繼承?

  1. 繼承是面向?qū)ο蟮乃拇筇匦灾?,用來表示類之間的 is-a 關(guān)系,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過復雜,也會影響到代碼的可維護性。所以,對于是否應該在項目中使用繼承,網(wǎng)上有很多爭議。很多人覺得繼承是一種反模式,應該盡量少用,甚至不用。為什么會有這樣的爭議?我們通過一個例子來解釋一下。
  2. 假設(shè)我們要設(shè)計一個關(guān)于鳥的類。我們將“鳥類”這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。
  3. 我們知道,大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 fly() 方法呢?答案是否定的。盡管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現(xiàn)實世界中事物的認識。當然,你可能會說,我在鴕鳥這個子類中重寫(override)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.'");
  }
}
  1. 這種設(shè)計思路雖然可以解決問題,但不夠優(yōu)美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對于這些不會飛的鳥來說,我們都需要重寫 fly() 方法,拋出異常。這樣的設(shè)計,一方面,徒增了編碼的工作量;另一方面,也違背了我們之后要講的最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。

  2. 你可能又會說,那我們再通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類,不就可以了嗎?具體的繼承關(guān)系如下圖所示:
    設(shè)計模式-01.設(shè)計思想

  3. 從圖中我們可以看出,繼承關(guān)系變成了三層。不過,整體上來講,目前的繼承關(guān)系還比較簡單,層次比較淺,也算是一種可以接受的設(shè)計思路。我們再繼續(xù)加點難度。在剛剛這個場景中,我們只關(guān)注“鳥會不會飛”,但如果我們還關(guān)注“鳥會不會叫”,那這個時候,我們又該如何設(shè)計類之間的繼承關(guān)系呢?

  4. 是否會飛?是否會叫?兩個行為搭配起來會產(chǎn)生四種情況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。如果我們繼續(xù)沿用剛才的設(shè)計思路,那就需要再定義四個抽象類(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。
    設(shè)計模式-01.設(shè)計思想

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

  6. 總之,繼承最大的問題就在于:繼承層次過深、繼承關(guān)系過于復雜會影響到代碼的可讀性和可維護性。這也是為什么我們不推薦使用繼承。那剛剛例子中繼承存在的問題,我們又該如何來解決呢?你可以先自己思考一下,再聽我下面的講解。

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

1. 實際上,我們可以利用組合(composition)、接口、委托(delegation)三個技術(shù)手段,一塊兒來解決剛剛繼承存在的問題。 2. 接口表示具有某種行為特性。針對“會飛”這樣一個行為特性,我們可以定義一個 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 Tweetable, EggLayable {//鴕鳥
  //... 省略其他屬性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
1. 不過,我們知道,接口只聲明方法,不定義實現(xiàn)。也就是說,每個會下蛋的鳥都要實現(xiàn)一遍 layEgg() 方法,并且實現(xiàn)邏輯是一樣的,這就會導致代碼重復的問題。那這個問題又該如何解決呢? 2. 我們可以針對三個接口再定義三個實現(xiàn)類,它們分別是:實現(xiàn)了 fly() 方法的 FlyAbility 類、實現(xiàn)了 tweet() 方法的 TweetAbility 類、實現(xiàn)了 layEgg() 方法的 EggLayAbility 類。然后,通過組合和委托技術(shù)來消除代碼重復。具體的代碼實現(xiàn)如下所示:
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 關(guān)系,支持多態(tài)特性,代碼復用。而這三個作用都可以通過其他技術(shù)手段來達成。比如 is-a 關(guān)系,我們可以通過組合和接口的 has-a 關(guān)系來替代;多態(tài)特性我們可以利用接口來實現(xiàn);代碼復用我們可以通過組合和委托來實現(xiàn)。所以,從理論上講,通過組合、接口、委托三個技術(shù)手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關(guān)系,特別是一些復雜的繼承關(guān)系。

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

  1. 盡管我們鼓勵多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。從上面的例子來看,繼承改寫成組合意味著要做更細粒度的類的拆分。這也就意味著,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的復雜程度和維護成本。所以,在實際的項目開發(fā)中,我們還是要根據(jù)具體的情況,來具體選擇該用繼承還是組合。
  2. 如果類之間的繼承結(jié)構(gòu)穩(wěn)定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關(guān)系),繼承關(guān)系不復雜,我們就可以大膽地使用繼承。反之,系統(tǒng)越不穩(wěn)定,繼承層次很深,繼承關(guān)系復雜,我們就盡量使用組合來替代繼承。
  3. 除此之外,還有一些設(shè)計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關(guān)系,而模板模式(template pattern)使用了繼承關(guān)系。
    4. 前面我們講到繼承可以實現(xiàn)代碼復用。利用繼承特性,我們把相同的屬性和方法,抽取出來,定義到父類中。子類復用父類中的屬性和方法,達到代碼復用的目的。但是,有的時候,從業(yè)務含義上,A 類和 B 類并不一定具有繼承關(guān)系。比如,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但并不具有繼承關(guān)系(既不是父子關(guān)系,也不是兄弟關(guān)系)。僅僅為了代碼復用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。如果不熟悉背后設(shè)計思路的同事,發(fā)現(xiàn) Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的卻只是 URL 相關(guān)的操作,會覺得這個代碼寫得莫名其妙,理解不了。這個時候,使用組合就更加合理、更加靈活。具體的代碼實現(xiàn)如下所示:
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();
  }
  //..
}

還有一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數(shù)的入?yún)㈩愋停雲(yún)⒂址墙涌?,為了支持多態(tài),只能采用繼承來實現(xiàn)。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權(quán)限去修改這部分代碼,但是我們希望能重寫這個類在運行時執(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);

盡管有些人說,要杜絕繼承,100% 用組合代替繼承,但是我的觀點沒那么極端!之所以“多用組合少用繼承”這個口號喊得這么響,只是因為,長期以來,我們過度使用繼承。還是那句話,組合并不完美,繼承也不是一無是處。只要我們控制好它們的副作用、發(fā)揮它們各自的優(yōu)勢,在不同的場合下,恰當?shù)剡x擇使用繼承還是組合,這才是我們所追求的境界。

如何通過封裝、抽象、模塊化、中間層等解耦代碼?

“解耦”為何如此重要?

  1. 軟件設(shè)計與開發(fā)最重要的工作之一就是應對復雜性。人處理復雜性的能力是有限的。過于復雜的代碼往往在可讀性、可維護性上都不友好。那如何來控制代碼的復雜性呢?手段有很多,我個人認為,最關(guān)鍵的就是解耦,保證代碼松耦合、高內(nèi)聚。如果說重構(gòu)是保證代碼質(zhì)量不至于腐化到無可救藥地步的有效手段,那么利用解耦的方法對代碼重構(gòu),就是保證代碼不至于復雜到無法控制的有效手段。
  2. 后文迪米特法則有介紹,什么是“高內(nèi)聚、松耦合”。。實際上,“高內(nèi)聚、松耦合”是一個比較通用的設(shè)計思想,不僅可以指導細粒度的類和類之間關(guān)系的設(shè)計,還能指導粗粒度的系統(tǒng)、架構(gòu)、模塊的設(shè)計。相對于編碼規(guī)范,它能夠在更高層次上提高代碼的可讀性和可維護性。
  3. 不管是閱讀代碼還是修改代碼,“高內(nèi)聚、松耦合”的特性可以讓我們聚焦在某一模塊或類中,不需要了解太多其他模塊或類的代碼,讓我們的焦點不至于過于發(fā)散,降低了閱讀和修改代碼的難度。而且,因為依賴關(guān)系簡單,耦合小,修改代碼不至于牽一發(fā)而動全身,代碼改動比較集中,引入 bug 的風險也就減少了很多。同時,“高內(nèi)聚、松耦合”的代碼可測試性也更加好,容易 mock 或者很少需要 mock 外部依賴的模塊或者類。
  4. 除此之外,代碼“高內(nèi)聚、松耦合”,也就意味著,代碼結(jié)構(gòu)清晰、分層和模塊化合理、依賴關(guān)系簡單、模塊或類之間的耦合小,那代碼整體的質(zhì)量就不會差。即便某個具體的類或者模塊設(shè)計得不怎么合理,代碼質(zhì)量不怎么高,影響的范圍是非常有限的。我們可以聚焦于這個模塊或者類,做相應的小型重構(gòu)。而相對于代碼結(jié)構(gòu)的調(diào)整,這種改動范圍比較集中的小型重構(gòu)的難度就容易多了。

代碼是否需要“解耦”?

  1. 那現(xiàn)在問題來了,我們該怎么判斷代碼的耦合程度呢?或者說,怎么判斷代碼是否符合“高內(nèi)聚、松耦合”呢?再或者說,如何判斷系統(tǒng)是否需要解耦重構(gòu)呢?
  2. 間接的衡量標準有很多,前面我們講到了一些,比如,看修改代碼會不會牽一發(fā)而動全身。除此之外,還有一個直接的衡量標準,也是我在閱讀源碼的時候經(jīng)常會用到的,那就是把模塊與模塊之間、類與類之間的依賴關(guān)系畫出來,根據(jù)依賴關(guān)系圖的復雜性來判斷是否需要解耦重構(gòu)。
  3. 如果依賴關(guān)系復雜、混亂,那從代碼結(jié)構(gòu)上來講,可讀性和可維護性肯定不是太好,那我們就需要考慮是否可以通過解耦的方法,讓依賴關(guān)系變得清晰、簡單。當然,這種判斷還是有比較強的主觀色彩,但是可以作為一種參考和梳理依賴的手段,配合間接的衡量標準一塊來使用。

如何給代碼“解耦”?

封裝與抽象

封裝和抽象作為兩個非常通用的設(shè)計思想,可以應用在很多設(shè)計場景中,比如系統(tǒng)、模塊、lib、組件、接口、類等等的設(shè)計。封裝和抽象可以有效地隱藏實現(xiàn)的復雜性,隔離實現(xiàn)的易變性,給依賴的模塊提供穩(wěn)定且易用的抽象接口

中間層

引入中間層能簡化模塊或類之間的依賴關(guān)系。下面這張圖是引入中間層前后的依賴關(guān)系對比圖。在引入數(shù)據(jù)存儲中間層之前,A、B、C 三個模塊都要依賴內(nèi)存一級緩存、Redis 二級緩存、DB 持久化存儲三個模塊。在引入中間層之后,三個模塊只需要依賴數(shù)據(jù)存儲一個模塊即可。從圖上可以看出,中間層的引入明顯地簡化了依賴關(guān)系,讓代碼結(jié)構(gòu)更加清晰
設(shè)計模式-01.設(shè)計思想
除此之外,我們在進行重構(gòu)的時候,引入中間層可以起到過渡的作用,能夠讓開發(fā)和重構(gòu)同步進行,不互相干擾。比如,某個接口設(shè)計得有問題,我們需要修改它的定義,同時,所有調(diào)用這個接口的代碼都要做相應的改動。如果新開發(fā)的代碼也用到這個接口,那開發(fā)就跟重構(gòu)沖突了。為了讓重構(gòu)能小步快跑,我們可以分下面四個階段來完成接口的修改。

  • 第一階段:引入一個中間層,包裹老的接口,提供新的接口定義。
  • 第二階段:新開發(fā)的代碼依賴中間層提供的新接口。
  • 第三階段:將依賴老接口的代碼改為調(diào)用新接口。
  • 第四階段:確保所有的代碼都調(diào)用新接口之后,刪除掉老的接口。

這樣,每個階段的開發(fā)工作量都不會很大,都可以在很短的時間內(nèi)完成。重構(gòu)跟開發(fā)沖突的概率也變小了。

模塊化

模塊化是構(gòu)建復雜系統(tǒng)常用的手段。不僅在軟件行業(yè),在建筑、機械制造等行業(yè),這個手段也非常有用。對于一個大型復雜系統(tǒng)來說,沒有人能掌控所有的細節(jié)。之所以我們能搭建出如此復雜的系統(tǒng),并且能維護得了,最主要的原因就是將系統(tǒng)劃分成各個獨立的模塊,讓不同的人負責不同的模塊,這樣即便在不了解全部細節(jié)的情況下,管理者也能協(xié)調(diào)各個模塊,讓整個系統(tǒng)有效運轉(zhuǎn)。

其他設(shè)計思想和原則

“高內(nèi)聚、松耦合”是一個非常重要的設(shè)計思想,能夠有效提高代碼的可讀性和可維護性,縮小功能改動導致的代碼改動范圍。實際上,在前面的章節(jié)中,我們已經(jīng)多次提到過這個設(shè)計思想。很多設(shè)計原則都以實現(xiàn)代碼的“高內(nèi)聚、松耦合”為目的。我們來一塊總結(jié)回顧一下都有哪些原則。

(這里沒有講的,在后面文章里)

  • 單一職責原則

我們前面提到,內(nèi)聚性和耦合性并非獨立的。高內(nèi)聚會讓代碼更加松耦合,而實現(xiàn)高內(nèi)聚的重要指導原則就是單一職責原則。模塊或者類的職責設(shè)計得單一,而不是大而全,那依賴它的類和它依賴的類就會比較少,代碼耦合也就相應的降低了。

  • 基于接口而非實現(xiàn)編程

基于接口而非實現(xiàn)編程能通過接口這樣一個中間層,隔離變化和具體的實現(xiàn)。這樣做的好處是,在有依賴關(guān)系的兩個模塊或類之間,一個模塊或者類的改動,不會影響到另一個模塊或類。實際上,這就相當于將一種強依賴關(guān)系(強耦合)解耦為了弱依賴關(guān)系(弱耦合)。依賴注入

  • 依賴注入

跟基于接口而非實現(xiàn)編程思想類似,依賴注入也是將代碼之間的強耦合變?yōu)槿躐詈?。盡管依賴注入無法將本應該有依賴關(guān)系的兩個類,解耦為沒有依賴關(guān)系,但可以讓耦合關(guān)系沒那么緊密,容易做到插拔替換

  • 多用組合少用繼承

我們知道,繼承是一種強依賴關(guān)系,父類與子類高度耦合,且這種耦合關(guān)系非常脆弱,牽一發(fā)而動全身,父類的每一次改動都會影響所有的子類。相反,組合關(guān)系是一種弱依賴關(guān)系,這種關(guān)系更加靈活,所以,對于繼承結(jié)構(gòu)比較復雜的代碼,利用組合來替換繼承,也是一種解耦的有效手段。

  • 迪米特法則

迪米特法則講的是,不該有直接依賴關(guān)系的類之間,不要有依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口。從定義上,我們明顯可以看出,這條原則的目的就是為了實現(xiàn)代碼的松耦合。至于如何應用這條原則來解耦代碼,你可以回過頭去閱讀一下第 22 講,這里我就不贅述了。除了上面講到的這些設(shè)計思想和原則之外,還有一些設(shè)計模式也是為了解耦依賴,比如觀察者模式,有關(guān)這一部分的內(nèi)容,我們留在設(shè)計模式模塊中慢慢講解。文章來源地址http://www.zghlxwxcb.cn/news/detail-471440.html

到了這里,關(guān)于設(shè)計模式-01.設(shè)計思想的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務器費用

相關(guān)文章

  • 二十四種設(shè)計模式與六大設(shè)計原則(三):【裝飾模式、迭代器模式、組合模式、觀察者模式、責任鏈模式、訪問者模式】的定義、舉例說明、核心思想、適用場景和優(yōu)缺點

    二十四種設(shè)計模式與六大設(shè)計原則(三):【裝飾模式、迭代器模式、組合模式、觀察者模式、責任鏈模式、訪問者模式】的定義、舉例說明、核心思想、適用場景和優(yōu)缺點

    接上次博客:二十四種設(shè)計模式與六大設(shè)計原則(二):【門面模式、適配器模式、模板方法模式、建造者模式、橋梁模式、命令模式】的定義、舉例說明、核心思想、適用場景和優(yōu)缺點-CSDN博客 目錄 裝飾模式【Decorator Pattern】 定義 舉例說明 核心思想 適用場景 優(yōu)缺點 迭代

    2024年04月17日
    瀏覽(25)
  • 【C++ 觀察者模式 思想理解】C++中的觀察者模式:松耦合設(shè)計與動態(tài)交互的藝術(shù),合理使用智能指針觀察者

    【C++ 觀察者模式 思想理解】C++中的觀察者模式:松耦合設(shè)計與動態(tài)交互的藝術(shù),合理使用智能指針觀察者

    在進入技術(shù)細節(jié)之前,理解觀察者模式(Observer Pattern)的基本概念和它在現(xiàn)代編程中的重要性是至關(guān)重要的。 觀察者模式是一種設(shè)計模式,它定義了對象間的一種一對多的依賴關(guān)系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并自動更新。在C++中,這個

    2024年01月24日
    瀏覽(48)
  • 扎實打牢數(shù)據(jù)結(jié)構(gòu)算法根基,從此不怕算法面試系列之010 week02 01-01 最簡單的排序算法-選擇排序法的設(shè)計思想

    接下類,我們學習另外一類非常基礎(chǔ)的算法,即排序算法。 排序算法是計算機科學領(lǐng)域研究的非常深入的一類算法,排序這個動作本身也是非常重要的, 很多時候面對無需的數(shù)據(jù),首先需要做的就是對他們進行排序。 排序算法——目的:讓數(shù)據(jù)有序。 排序算法——種類:種

    2023年04月21日
    瀏覽(29)
  • Spring核心設(shè)計思想

    Spring核心設(shè)計思想

    目錄 前言: Spring是什么 什么是IoC 傳統(tǒng)開發(fā)思想 IoC開發(fā)思想 Spring IoC 什么是DI 小結(jié): ? ? 官網(wǎng)中提出:Spring makes programming Java quicker, easier, and safer for everybody. Spring’s focus on speed, simplicity, and productivity has made it the?world\\\'s most popular Java framework. ? ? Spring 使編程 Java 對每個人來

    2023年04月17日
    瀏覽(21)
  • 算法設(shè)計思想——動態(tài)規(guī)劃

    算法設(shè)計思想——動態(tài)規(guī)劃

    是一種常見的算法設(shè)計方法,用于解決一類重疊子問題的優(yōu)化問題。他的基本思想是將問題分解成多個重疊的子問題,遞歸求解,并將子問題的求解緩存起來,避免重復計算,從而得到問題的解。 動態(tài)規(guī)劃通常適用于以下兩個條件的問題: 1.重疊子問題:原問題可以分解為若

    2024年02月03日
    瀏覽(22)
  • 閃電網(wǎng)絡協(xié)議設(shè)計思想剖析

    閃電網(wǎng)絡協(xié)議設(shè)計思想剖析

    閃電網(wǎng)絡可能是比特幣之上部署的最受期待的技術(shù)創(chuàng)新。閃電網(wǎng)絡,為由 Joseph Poon 和 Tadge Dryja 于2015年首次提出的支付層,承諾支持: 用戶之間幾乎無限數(shù)量的鏈下交易, 幾乎免費, 同時利用比特幣提供的安全性。 2016年時,至少三個公司——Poon 和 Dryja 的 Lightning、 Block

    2024年03月20日
    瀏覽(27)
  • Spring 核心與設(shè)計思想

    ??作者:銀河罐頭 ??系列專欄:JavaEE ?? “種一棵樹最好的時間是十年前,其次是現(xiàn)在” 通常所說的 Spring 指的是 Spring Framework(Spring 框架)。 Spring 是包含多種工具方法的 IoC 容器。 IoC(Inversion of Control): 控制反轉(zhuǎn) \\\"控制反轉(zhuǎn)\\\"又是什么意思? 下面以一個程序來舉例。 假如我

    2024年02月02日
    瀏覽(24)
  • 【Spring】核心與設(shè)計思想

    【Spring】核心與設(shè)計思想

    ?哈嘍,哈嘍,大家好~ 我是你們的老朋友: 保護小周??? 談起Java 圈子里的框架,最年長最耀眼的莫過于 Spring 框架啦,如今已成為最流行、最廣泛使用的Java開發(fā)框架之一。不知道大家有沒有在使用 Spring 框架的時候思考過這些問題, 什么是框架?Spring 是什么?如何理解

    2024年02月08日
    瀏覽(28)
  • Spring框架核心與設(shè)計思想

    Spring框架核心與設(shè)計思想

    我們一般所說的Spring指的是Spring Framework(Spring 框架),它是一個開源的框架,Spring支持廣泛的應用場景,它可以讓Java企業(yè)級的應用程序開發(fā)變得更簡單,官方一點的回答:spring是J2EE應用程序框架,是輕量級的IoC和AOP的容器框架,主要是針對javaBean的生命周期進行管理的輕量級

    2023年04月15日
    瀏覽(35)
  • 從架構(gòu)設(shè)計思想出發(fā)看Flutter

    Flutter 是一種流行的移動應用程序開發(fā)框架,它的設(shè)計特點之一是可以使用單一代碼庫構(gòu)建 iOS 和 Android 應用程序。然而,對于功能比較多、模塊比較復雜的應用程序,僅憑單一的代碼庫就可能導致代碼的復雜性和維護難度的增加。在這種情況下,通過合適的應用程序架構(gòu)設(shè)計

    2024年02月07日
    瀏覽(22)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包