天天看點

設計模式六大原則-之1-3

計模式六大原則1—單一職責原則

單一職責原則(SingleResponsibility Principle ,SRP)

定義:應該有且隻有一個原因引起類的變更。

問題由來:類T負責兩個不同的職責:職責P1,職責P2。當由于職責P1需求發生改變而需要修改類T時,有可能會導緻原本運作正常的職責P2功能發生故障。

解決方案:遵循單一職責原則。分别建立兩個類T1、T2,使T1完成職責P1功能,T2完成職責P2功能。這樣,當修改類T1時,不會使職責P2發生故障風險;同理,當修改T2時,也不會使職責P1發生故障風險。

    其實,一般在程式設計中,我們會有意識地遵守這一原則,這也是常識。但是即便是經驗豐富的程式員寫出的程式,也會有違背這一原則的代碼存在。為什麼會出現這種現象呢?因為有職責擴散。所謂職責擴散,就是因為某種原因,職責P被分化為粒度更細的職責P1和P2。

    比如:類T隻負責一個職責P,這樣設計是符合單一職責原則的。後來由于某種原因,也許是需求變更了,也許是程式的設計者境界提高了,需要将職責P細分為粒度更細的職責P1,P2,這時如果要使程式遵循單一職責原則,需要将類T也分解為兩個類T1和T2,分别負責P1、P2兩個職責。但是在程式已經寫好的情況下,這樣做簡直太費時間了。是以,簡單的修改類T,用它來負責兩個職責是一個比較不錯的選擇,雖然這樣做有悖于單一職責原則。(這樣做的風險在于職責擴散的不确定性,因為我們不會想到這個職責P,在未來可能會擴散為P1,P2,P3,P4……Pn。是以記住,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構。)

  舉個例子,比如有如下的接口IUserInfo:

設計模式六大原則-之1-3

那麼,這個接口承擔了使用者屬性操作和增加/删除使用者操作的職責。按照單一職責原則,應該把使用者資訊抽取成BO(Bussiness Object,業務對象),把行為抽取成一個Biz(Bussiness Logic,業務邏輯),按照這個思路進行修改,那麼就要重新拆封成2個接口,IUserBO負責使用者屬性,IUserBiz負責使用者的行為。如下圖:

設計模式六大原則-之1-3

好處:

1)、類的複雜性降低,實作的職責都有清晰明确的定義;

2)、可讀性和可維護性提高;

3)、變更引起的風險降低。

  需要說明的一點是單一職責原則不隻是面向對象程式設計思想所特有的,隻要是子產品化的程式設計,都适用單一職責原則。

設計模式六大原則2—裡氏替換原則

        裡氏替換原則(Liskov Substitution Principle, LSP)

看到裡氏替換原則,感覺很好奇,名字很怪,哈哈哈,其實這項原則最早是在1988年,由麻省理工學院的一位姓裡的女士(Barbara Liskov)提出來的,向偉大的IT屆的女精英們緻敬!

定義1:如果對應類型為S的對象o1,有類型為T的對象o2,使得以T定義的所有程式P,在所有的對象o1都替換成o2時,程式P的行為沒有發生變化,那麼類型S是類型T的子類型。

定義2:所有引用基類的地方都必須能夠透明地使用其子類的對象。

問題由來:有一功能P1,由類A完成。現需要對功能P1進行擴充,擴充後的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導緻原有功能P1發生故障。

解決方案:當使用繼承時,遵循裡氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的非抽象方法,也盡量不要重載父類A的方法。

      繼承包含這樣一層含義:父類中凡是已經實作好的方法(相對于抽象方法而言),實際上是在設定一系列的規範和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而裡氏替換原則就是表達了這一層含義。

     繼承作為面向對象三大特性之一,在給程式設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程式帶來侵入性,子類在繼承父類的同時,會對父類中的非抽象方法進行重寫或重載,那麼在一定程度上污染了父類;此外,程式的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改後,所有涉及到子類的功能都有可能會産生故障。

       舉例說明繼承的風險,我們需要完成一個兩數相減的功能,由類A來負責。特别說明:以下的例子來自于網友卡奴達摩的專欄,在此特别感謝!

[java]view plaincopy

  1. class A{  
  2. publicint func1(int a, int b){  
  3. return a-b;  
  4.    }  
  5. }  
  6. publicclass Client{  
  7. publicstaticvoid main(String[] args){  
  8.        A a = new A();  
  9.        System.out.println("100-50="+a.func1(100, 50));  
  10.        System.out.println("100-80="+a.func1(100, 80));  
  11.    }  
  12. }  

運作結果:

100-50=50

100-80=20

       後來,我們需要增加一個新的功能:完成兩數相加,然後再與100求和,由類B來負責。即類B需要完成兩個功能:

  • 兩數相減。
  • 兩數相加,然後再加100。

       由于類A已經實作了第一個功能,是以類B繼承類A後,隻需要再完成第二個功能就可以了,代碼如下:

[java]view plaincopy

  1. class B extends A{  
  2. publicint func1(int a, int b){  
  3. return a+b;  
  4.    }  
  5. publicint func2(int a, int b){  
  6. return func1(a,b)+100;  
  7.    }  
  8. }  
  9. publicclass Client{  
  10. publicstaticvoid main(String[] args){  
  11.        B b = new B();  
  12.        System.out.println("100-50="+b.func1(100, 50));  
  13.        System.out.println("100-80="+b.func1(100, 80));  
  14.        System.out.println("100+20+100="+b.func2(100, 20));  
  15.    }  
  16. }  

類B完成後,運作結果:

100-50=150

100-80=180

100+20+100=220

我們發現原本運作正常的相減功能發生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法,造成所有運作相減功能的代碼全部調用了類B重寫後的方法,造成原本運作正常的功能出現了錯誤。在實際程式設計中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可複用性會比較差,特别是運用多态比較頻繁時,程式運作出錯的幾率非常大。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合,組合等關系代替。

裡氏替換原則通俗的來講就是:子類可以擴充父類的功能,但不能改變父類原有的功能。它包含以下4層含義:

  • 子類可以實作父類的抽象方法,但不能覆寫父類的非抽象方法。
  • 子類中可以增加自己特有的方法。
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。即覆寫或者實作父類的方法時輸入的參數可以被放大。                               “契約優先”的原則,就是接口,這種設計方法也叫做Design by Contract.                                                             前置條件就是你要讓我執行,就必須滿足我的條件;後置條件就是我執行完了需要回報。
  • 當子類的方法實作父類的抽象方法時,方法的後置條件(即方法的傳回值)要比父類更嚴格。即

    覆寫或者實作父類的方法時輸出的結果可以被縮小。

       父類的一個方法的傳回值是一個類型T,子類的相同方法(重載或覆寫)的傳回值為S,那麼裡氏替換原則就要求S必須小于等于T,也就是說要麼S和T是同一個類型,要麼S是T的子類。

    後兩層含義其實就是:繼承類方法必須接受任何基類方法能接受的任何條件(參數)。同樣,繼承類必須順從基類的所有後續條件。這樣,我們就有了基于合同的LSP,基于合同的LSP是LSP的一種強化。

    好處:

    增強程式的健壯性,版本更新時也可以保持非常好的相容性。即使增加子類,原有的子類還可以繼續運作。

設計模式六大原則3—依賴倒置原則

2012-08-10 22:38192人閱讀評論(0)收藏舉報 

依賴倒置原則(Dependence Inversion Principle,DIP)

定義:依賴倒置原則具有以下三層含義:

1、高層子產品不應該依賴底層子產品,兩者都應該依賴其抽象;

2、抽象不應該依賴細節;

3、細節應該依賴抽象。

問題由來:類A直接依賴類B,若要将類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般為高層子產品,負責複雜的業務邏輯;類B和類C是低層子產品,負責基本的原子操作;假如修改類A,會給程式帶來不必要的風險。

解決方案:将類A修改為直接依賴接口I,類B和類C實作接口I,這樣類A通過接口I和類B或者類C發生聯系。

我們用一個例子說明依賴倒置原則。如下述代碼所示:

[java]view plaincopyprint?

  1. publicclass BMWCar {  
  2. publicvoid run(){  
  3.        System.out.println("BMW is runing.....");  
  4.    }  
  5. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass BMWCar {  
  2. publicvoid run(){  
  3.        System.out.println("BMW is runing.....");  
  4.    }  
  5. }  

[java]view plaincopyprint?

  1. publicclass Driver {  
  2. publicvoid drive(BMWCar bmw){  
  3.        System.out.println("Driver is driving");  
  4.        bmw.run();  
  5.    }  
  6. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass Driver {  
  2. publicvoid drive(BMWCar bmw){  
  3.        System.out.println("Driver is driving");  
  4.        bmw.run();  
  5.    }  
  6. }  

[java]view plaincopyprint?

  1. publicclass Client {  
  2. publicstaticvoid main(String[] args) {  
  3. // TODO Auto-generated method stub
  4.        Driver driver=new Driver();  
  5.        driver.drive(new BMWCar());  
  6.    }  
  7. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass Client {  
  2. publicstaticvoid main(String[] args) {  
  3. // TODO Auto-generated method stub
  4.        Driver driver=new Driver();  
  5.        driver.drive(new BMWCar());  
  6.    }  
  7. }  

  那現在如果司機開的是Benz的車,那麼我們就得要修改Driver類的drive行為了。那如果司機還開别的類型的車,比如Bick等,那我們豈不是都是對Driver的drive行為作出更改。這是什麼原因呢?因為Driver和BMWCar之間的耦合度太強了!

  是以我們引入一個抽象的接口ICar,Driver類與ICar發生依賴關系,BMWCar和BenzCar等實作ICar.

[java]view plaincopyprint?

  1. publicinterface ICar {  
  2. publicvoid run();  
  3. }  

Java代碼

設計模式六大原則-之1-3
  1. publicinterface ICar {  
  2. publicvoid run();  
  3. }  

[java]view plaincopyprint?

  1. publicclass BMWCar implements ICar{  
  2. publicvoid run(){  
  3.        System.out.println("BMW is runing.....");  
  4.    }  
  5. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass BMWCar implements ICar{  
  2. publicvoid run(){  
  3.        System.out.println("BMW is runing.....");  
  4.    }  
  5. }  

[java]view plaincopyprint?

  1. publicclass BenzCar implements ICar {  
  2. publicvoid run(){  
  3.        System.out.println("Benz is runing.....");  
  4.    }  
  5. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass BenzCar implements ICar {  
  2. publicvoid run(){  
  3.        System.out.println("Benz is runing.....");  
  4.    }  
  5. }  

[java]view plaincopyprint?

  1. publicclass Driver {  
  2. publicvoid drive(ICar car){  
  3.        System.out.println("Driver is driving");  
  4.        car.run();  
  5.    }  
  6. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass Driver {  
  2. publicvoid drive(ICar car){  
  3.        System.out.println("Driver is driving");  
  4.        car.run();  
  5.    }  
  6. }  

[java]view plaincopyprint?

  1. publicclass Client {  
  2. publicstaticvoid main(String[] args) {  
  3. // TODO Auto-generated method stub
  4.        Driver driver=new Driver();  
  5.        driver.drive(new BMWCar());  
  6.        driver.drive(new BenzCar());  
  7.    }  
  8. }  

Java代碼

設計模式六大原則-之1-3
  1. publicclass Client {  
  2. publicstaticvoid main(String[] args) {  
  3. // TODO Auto-generated method stub
  4.        Driver driver=new Driver();  
  5.        driver.drive(new BMWCar());  
  6.        driver.drive(new BenzCar());  
  7.    }  
  8. }  

這樣修改後,無論以後怎樣擴充Client類,都不需要再修改Driver類了。這隻是一個簡單的例子,實際情況中,代表高層子產品的Driver類将負責完成主要的業務邏輯,一旦需要對它進行修改,引入錯誤的風險極大。是以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程式造成的風險。

 從這個例子,我們可以看出,依賴倒置原則的核心思想是面向接口程式設計。在java中,抽象指的是接口或者抽象類,細節就是具體的實作類,使用接口或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實作類去完成。相對于細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。

    依賴關系有三種方式:

1)接口傳遞依賴對象,如上述例子中使用的方法是接口傳遞;

2)構造方法傳遞依賴對象;

3)setter方法傳遞依賴對象。

   在實際程式設計中,對于依賴倒置原則的使用,我們需要做到如下3點:

  • 低層子產品盡量都要有抽象類或接口,或者兩者都有。
  • 變量的聲明類型盡量是抽象類或接口。
  • 任何類都不應該從具體類派生。
  • 盡量不要覆寫基類的方法。
  • 使用繼承時遵循裡氏替換原則。

轉載于:https://blog.51cto.com/xeomiracle/1398705