設計模式六大原則-之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:

6aff89b0-946f-4bcb-9af0-bfa5e92eab17[1].

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


d5e0298b-6e6a-4993-aa0c-cf314a007332[1].

好處:

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.  

  7. publicclass Client{  

  8. publicstaticvoid main(String[] args){  

  9.        A a = new A();  

  10.        System.out.println("100-50="+a.func1(10050));  

  11.        System.out.println("100-80="+a.func1(10080));  

  12.    }  

  13. }  


運行結果:

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.  

  6. publicint func2(int a, int b){  

  7. return func1(a,b)+100;  

  8.    }  

  9. }  

  10.  

  11. publicclass Client{  

  12. publicstaticvoid main(String[] args){  

  13.        B b = new B();  

  14.        System.out.println("100-50="+b.func1(10050));  

  15.        System.out.println("100-80="+b.func1(10080));  

  16.        System.out.println("100+20+100="+b.func2(10020));  

  17.    }  

  18. }  

類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—依賴倒置原則

192人閱讀評論(0)收藏舉報 

依賴倒置原則(Dependence Inversion PrincipleDIP)

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

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.  

  3. publicvoid run(){  

  4.        System.out.println("BMW is runing.....");  

  5.    }  

  6.  

  7. }  

Java代碼收藏代碼

  1. publicclass BMWCar {  

  2.  

  3. publicvoid run(){  

  4.        System.out.println("BMW is runing.....");  

  5.    }  

  6.  

  7. }  

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

  3. /**

  4.     * @param args

  5.     */

  6. publicstaticvoid main(String[] args) {  

  7. // TODO Auto-generated method stub

  8.        Driver driver=new Driver();  

  9.        driver.drive(new BMWCar());  

  10.  

  11.    }  

  12.  

  13. }  

Java代碼收藏代碼

  1. publicclass Client {  

  2.  

  3. /**

  4.     * @param args

  5.     */

  6. publicstaticvoid main(String[] args) {  

  7. // TODO Auto-generated method stub

  8.        Driver driver=new Driver();  

  9.        driver.drive(new BMWCar());  

  10.  

  11.    }  

  12.  

  13. }  

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

  因此我們引入一個抽象的接口ICar,Driver類與ICar發生依賴關係,BMWCar和BenzCar等實現ICar.

 

[java]view plaincopyprint?

  1. publicinterface ICar {  

  2. publicvoid run();  

  3.  

  4. }  

Java代碼收藏代碼

  1. publicinterface ICar {  

  2. publicvoid run();  

  3.  

  4. }  

[java]view plaincopyprint?

  1. publicclass BMWCar implements ICar{  

  2.  

  3. publicvoid run(){  

  4.        System.out.println("BMW is runing.....");  

  5.    }  

  6.  

  7. }  

Java代碼收藏代碼

  1. publicclass BMWCar implements ICar{  

  2.  

  3. publicvoid run(){  

  4.        System.out.println("BMW is runing.....");  

  5.    }  

  6.  

  7. }  

[java]view plaincopyprint?

  1. publicclass BenzCar implements ICar {  

  2.  

  3. publicvoid run(){  

  4.        System.out.println("Benz is runing.....");  

  5.    }  

  6. }  

Java代碼收藏代碼

  1. publicclass BenzCar implements ICar {  

  2.  

  3. publicvoid run(){  

  4.        System.out.println("Benz is runing.....");  

  5.    }  

  6. }  

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

  3. /**

  4.     * @param args

  5.     */

  6. publicstaticvoid main(String[] args) {  

  7. // TODO Auto-generated method stub

  8.        Driver driver=new Driver();  

  9.        driver.drive(new BMWCar());  

  10.        driver.drive(new BenzCar());  

  11.  

  12.    }  

  13. }  

Java代碼收藏代碼

  1. publicclass Client {  

  2.  

  3. /**

  4.     * @param args

  5.     */

  6. publicstaticvoid main(String[] args) {  

  7. // TODO Auto-generated method stub

  8.        Driver driver=new Driver();  

  9.        driver.drive(new BMWCar());  

  10.        driver.drive(new BenzCar());  

  11.  

  12.    }  

  13. }  

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

 

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

    依賴關係有三種方式:

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

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

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

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

  • 低層模塊儘量都要有抽象類或接口,或者兩者都有。

  • 變量的聲明類型儘量是抽象類或接口。

  • 任何類都不應該從具體類派生。

  • 儘量不要覆寫基類的方法。

  • 使用繼承時遵循里氏替換原則。

 

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