《設計模式之禪》第二版 學習之六大設計原則(二)

昨天學習了六個設計原則中的單一職責原則和里氏替換原則,今天繼續學習依賴倒置原則和接口隔離原則,因爲都是一些偏理論的東西,雖說理解,但在使用中還是會比較喫力,建議沒事的時候多回過頭來看幾遍,孰能生巧,用起來也會得心應手。

依賴倒置原則

依賴倒置原則(Dependence Inversion Principle,DIP)這個名字看着有點彆扭,“依
賴”還“倒置”,這到底是什麼意思?
依賴倒置原則的原始定義是:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.翻譯過來,包含三層含義:


  • 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;
  • 抽象不應該依賴細節;
  • 細節應該依賴抽象。

高層模塊和低層模塊容易理解,每一個邏輯的實現都是由原子邏輯組成的,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊。
那什麼是抽象?什麼又是細節呢?在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,實現接口或繼承抽象類而產生的類就是細節,其特點就是可以直接被實例化,也就是可以加上一個關鍵字new產生一個對象。依賴倒置原則在Java語言中的表現就是:

  • 模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過接口或抽象類產生的;
  • 接口或抽象類不依賴於實現類;
  • 實現類依賴接口或抽象類。
    更加精簡的定義就是“面向接口編程”——OOD(Object-Oriented Design,面向對象設
    計)的精髓之一。

  • 我們通過一個例子來說明。現在的汽車越來越便宜了,一個衛生間的造價就可以買到一輛不錯的汽車,有汽車就必然有人來駕駛,司機駕駛奔馳車的類圖如圖3-1所示。
    這裏寫圖片描述

    奔馳車可以提供一個方法run,代表車輛運行,實現過程如代碼清單3-1所示。

    public class Driver {//司機的主要職責就是駕駛汽車
     public void drive(Benz benz){
      benz.run();
     }
    }

    司機通過調用奔馳車的run方法開動奔馳車,其源代碼如下所示。

    public class Benz {//汽車肯定會跑
     public void run(){
      System.out.println("奔馳汽車開始運行...");
     }
    }

    有車,有司機,在Client場景類產生相應的對象,其源代碼如下所示。

    public class Client {
      public static void main(String[] args) {
       Driver zhangSan = new Driver();
       Benz benz = new Benz();
       zhangSan.drive(benz); //張三開奔馳車
      }
    }

    通過以上的代碼,完成了司機開動奔馳車的場景,到目前爲止,這個司機開奔馳車的項目沒有任何問題。但是業務需求變更永無休止,技術前進就永無止境,在發生變更時才能發覺我們的設計或程序是否是松耦合。我們在一段貌似磐石的程序上加上一塊小石頭:張三司機不僅要開奔馳車,還要開寶馬車,又該怎麼實現呢?麻煩出來了,那好,我們走一步是一步,我們先把寶馬車產生出來。

    public class BMW {//寶馬車當然也可以開動了
      public void run(){
       System.out.println("寶馬汽車開始運行...");
      }
    }

    寶馬車也產生了,但是我們卻沒有辦法讓張三開動起來,爲什麼?張三沒有開動寶馬車的方法!這就是我們的設計出現了問題,司機類和奔馳車類之間是緊耦合的關係,其導致的結果就是系統的可維護性大大降低,可讀性降低,兩個相似的類需要閱讀兩個文件,你樂意嗎?還有穩定性,什麼是穩定性?固化的、健壯的纔是穩定的,這裏只是增加了一個車類就需要修改司機類,這不是穩定性,這是易變性。被依賴者的變更竟然讓依賴者來承擔修改的成本,這樣的依賴關係誰肯承擔!

    設計是否具備穩定性,只要適當地“鬆鬆土”,觀察“設計的藍圖”是否還可以茁壯地成長就可以得出結論,穩定性較高的設計,在周圍環境頻繁變化的時候,依然可以做到“我自巋然不動”。

    根據以上證明,如果不使用依賴倒置原則就會加重類間的耦合性,降低系統的穩定性,
    增加並行開發引起的風險,降低代碼的可讀性和可維護性。承接上面的例子,引入依賴倒置
    原則後的類圖如圖3-2所示。
    這裏寫圖片描述

    建立兩個接口:IDriver和ICar,分別定義了司機和汽車的各個職能,司機就是駕駛汽
    車,必須實現drive()方法,其實現過程如代碼所示。

    public interface IDriver {
     //是司機就應該會駕駛汽車
     public void drive(ICar car);
    }
    public class Driver implements IDriver{
    //司機的主要職責就是駕駛汽車
     public void drive(ICar car){
      car.run();
     }
    }

    在IDriver中,通過傳入ICar接口實現了抽象之間的依賴關係,Driver實現類也傳入了ICar接口,至於到底是哪個型號的Car,需要在高層模塊中聲明。

    public interface ICar {
     //是汽車就應該能跑
     public void run();
    }
    public class Benz implements ICar{
     //汽車肯定會跑
     public void run(){
       System.out.println("奔馳汽車開始運行...");
     }
    }
    public class BMW implements ICar{
     //寶馬車當然也可以開動了
     public void run(){
      System.out.println("寶馬汽車開始運行...");
     }
    }

    在業務場景中,我們貫徹“抽象不應該依賴細節”,也就是我們認爲抽象(ICar接口)不依賴BMW和Benz兩個實現類(細節),因此在高層次的模塊中應用都是抽象。

    public class Client {
     public static void main(String[] args) {
      IDriver zhangSan = new Driver();
      ICar benz = new Benz();
      //張三開奔馳車
      zhangSan.drive(benz);
     }
    }

    Client屬於高層業務邏輯,它對低層模塊的依賴都建立在抽象上,zhangSan的表面類型是IDriver,Benz的表面類型是ICar,也許你要問,在這個高層模塊中也調用到了低層模塊,比如new Driver()和new Benz()等,如何解釋?確實如此,zhangSan的表面類型是IDriver,是一個接口,是抽象的、非實體化的,在其後的所有操作中,zhangSan都是以IDriver類型進行操作,屏蔽了細節對抽象的影響。當然,張三如果要開寶馬車,也很容易,我們只要修改業務場景類就可以,

    public class Client {
     public static void main(String[] args) {
       IDriver zhangSan = new Driver();
       ICar bmw = new BMW();
       //張三開奔馳車
       zhangSan.drive(bmw);
     }
    }

    在新增加低層模塊時,只修改了業務場景類,也就是高層模塊對其他低層模塊如Driver類不需要做任何修改,業務就可以運行,把“變更”引起的風險擴散降到最低。
    在Java中,只要定義變量就必然要有類型,一個變量可以有兩種類型:表面類型和實際類型,表面類型是在定義的時候賦予的類型,實際類型是對象的類型,如zhangSan的表面類型是IDriver,實際類型是Driver。
    對象的依賴關係有三種方式來傳遞,如下所示。
    1. 構造函數傳遞依賴對象
    在類中通過構造函數聲明依賴對象,按照依賴注入的說法,這種方式叫做構造函數注
    入。

    public interface IDriver {
    //是司機就應該會駕駛汽車
    public void drive();
    }
    public class Driver implements IDriver{
     private ICar car;
     //構造函數注入
     public Driver(ICar _car){
      this.car = _car;
     }
    //司機的主要職責就是駕駛汽車
     public void drive(){
      this.car.run();
     }
    }

    2.Setter方法傳遞依賴對象
    在抽象中設置Setter方法聲明依賴關係,依照依賴注入的說法,這是Setter依賴注入。

    public interface IDriver {
     //車輛型號
     public void setCar(ICar car);
      //是司機就應該會駕駛汽車
      public void drive();
     }
     public class Driver implements IDriver{
      private ICar car;
      public void setCar(ICar car){
      this.car = car;
     }
     //司機的主要職責就是駕駛汽車
     public void drive(){
      this.car.run();
     }
    }

    3.接口聲明依賴對象
    在接口的方法中聲明依賴對象,上面奔馳,寶馬的例子就採用了接口聲明依賴的方式,該方法也叫做接口注入。

    依賴倒置原則的本質就是通過抽象(接口或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合,我們怎麼在項目中使用這個規則呢?只要遵循以下的幾個規則就可以:
    每個類儘量都有接口或抽象類,或者抽象類和接口兩者都具備
    ● 變量的表面類型儘量是接口或者是抽象類
    ● 任何類都不應該從具體類派生
    ● 儘量不要覆寫基類的方法
    ● 結合里氏替換原則使用

    接口隔離原則

    在講接口隔離原則之前,先明確一下我們的主角——接口。接口分爲兩種:
    ● 實例接口(Object Interface),在Java中聲明一個類,然後用new關鍵字產生一個實例,它是對一個類型的事物的描述,這是一種接口。比如你定義Person這個類,然後使用Person zhangSan=new Person()產生了一個實例,這個實例要遵從的標準就是Person這個類,Person類就是zhangSan的接口。疑惑?看不懂?不要緊,那是因爲讓Java語言浸染的時間太長了,只要知道從這個角度來看,Java中的類也是一種接口。
    ● 類接口(Class Interface),Java中經常使用的interface關鍵字定義的接口。
    主角已經定義清楚了,那什麼是隔離呢?它有兩種定義,如下所示:

    • Clients should not be forced to depend upon interfaces that they don’t use.(客戶端不應該依賴它不需要的接口。)
    • The dependency of one class to another one should depend on the smallest possible interface.(類間的依賴關係應該建立在最小的接口上。)
      我們把這兩個定義剖析一下,先說第一種定義:“客戶端不應該依賴它不需要的接口”,那依賴什麼?依賴它需要的接口,客戶端需要什麼接口就提供什麼接口,把不需要的接口剔除掉,那就需要對接口進行細化,保證其純潔性;再看第二種定義:“類間的依賴關係應該建立在最小的接口上”,它要求是最小的接口,也是要求接口細化,接口純潔,與第一個定義如出一轍,只是一個事物的兩種不同描述。
      我們可以把這兩個定義概括爲一句話:建立單一接口,不要建立臃腫龐大的接口。再通俗一點講:接口儘量細化,同時接口中的方法儘量少。
      這與單一職責原則不是相同的嗎?錯,接口隔離原則與單一職責的審視角度是不相同的,單一職
      責要求的是類和接口職責單一,注重的是職責,這是業務邏輯上的劃分,而接口隔離原則要
      求接口的方法儘量少。

    接口隔離原則是對接口進行規範約束,其包含以下4層含義:
    ● 接口要儘量小

    這是接口隔離原則的核心定義,不出現臃腫的接口(Fat Interface),但是“小”是有限度
    的,首先就是不能違反單一職責原則,並且根據接口隔離原則拆分接口時,首先必須滿足單一職責原則。
    ● 接口要高內聚
    高內聚就是提高接口、類、模塊的處理能力,減少對外的交互。
    ● 定製服務
    一個系統或系統內的模塊之間必然會有耦合,有耦合就要有相互訪問的接口(並不一定就是Java中定義的Interface,也可能是一個類或單純的數據交換),我們設計時就需要爲各個訪問者(即客戶端)定製服務,什麼是定製服務?定製服務就是單獨爲一個個調用者供最合適的接口。減少接口中的無用方法。
    ● 接口設計是有限度的
    接口的設計粒度越小,系統越靈活,這是不爭的事實。但是,靈活的同時也帶來了結構
    的複雜化,開發難度增加,可維護性降低,這不是一個項目或產品所期望看到的,所以接口
    設計一定要注意適度,這個“度”如何來判斷呢?根據經驗和常識判斷,沒有一個固化或可測
    量的標準。


    接口隔離原則是對接口的定義,同時也是對類的定義,接口和類儘量使用原子接口或原子類來組裝。但是,這個原子該怎麼劃分是設計模式中的一大難題,在實踐中可以根據以下幾個規則來衡量:
    ● 一個接口只服務於一個子模塊或業務邏輯;
    ● 通過業務邏輯壓縮接口中的public方法,接口時常去回顧,儘量讓接口達到“滿身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
    ● 已經被污染了的接口,儘量去修改,若變更的風險較大,則採用適配器模式進行轉化處理;
    ● 瞭解環境,拒絕盲從。每個項目或產品都有特定的環境因素,別看到大師是這樣做的你就照抄。千萬別,環境不同,接口拆分的標準就不同。深入瞭解業務邏輯,最好的接口設計就出自你的手中!

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章