1. 程式人生 > >設計模式之6大設計原則

設計模式之6大設計原則

單一職責原則

單一職責原則(Single Responsibility Principle, SRP)的定義是: 應該有且僅有一個原因引起類或介面的變更。即一個類或介面只負責一個功能領域中的相應職責。

單一職責原則提出了一個編寫程式的標準, 它使類的複雜性降低、提高了程式碼的可讀性、可維護性和可擴充套件性、並降低了類或介面變更而引起的風險。但在實際專案中, 我們通常對"職責"沒有一個量化的標準, 比如一個類到底要負責哪些職責?這些職責應該怎麼細化?這種不可度量的外部因素給單一職責原則的實踐帶來了一定的困難性。

所以, 單一職責原則固然是一種非常好的理念, 但如果只會生搬硬套, 卻又會引起類的劇增, 給維護帶來很多不必要的麻煩, 而且過分細分類的職責(比如將一個類拆分成多個類, 類之間組合或聚合在一起)也會人為增加系統的複雜性。 

綜上所述, 單一職責原則由於"職責"的不可度量性, 需要設計人員具有較強的分析設計能力和相關實踐能力。在實際運用中, 給出的建議是: 介面一定要做到單一職責, 類的設計儘量做到只有一個原因引起變更。

里氏替換原則

里氏替換原則(Liskov Substitution Principle, LSP)最早是在1988年, 由麻省理工學院的一位姓裡的女士(Barbara Liskov)提出來的, 它有兩種定義: 

  • 第一種定義: 如果對每一個型別為 T1的物件 o1,都有型別為 T2 的物件o2,使得以 T1定義的所有程式 P 在所有的物件 o1 都代換成 o2 時,程式 P 的行為沒有發生變化,那麼型別 T2 是型別 T1 的子型別。
  • 第二種定義: 所有引用基類的地方必須能透明地使用其子類的物件。

第二種定義是最清晰明確的, 即在軟體中將一個基類物件替換成它的子類物件,程式將不會產生任何錯誤或異常,反過來則不成立,如果軟體中使用的是一個子類物件的話,那麼它不一定能夠替換成它的基類物件。

里氏替換原則為良好的繼承定義了一個規範: 子類可以擴充套件父類的功能, 但不能改變父類原有的功能。具體可概括成以下4點: 

子類可以實現父類的抽象方法, 但不能覆寫父類的非抽象方法

public class Parent {
    public int sum(int a, int b) {
        
return a + b; } } public class Son extends Parent { @Override public int sum(int a, int b) { //doSomething, 不一定是求和, 即父類原有的功能已經改變 return a - b; } public void study() { } }

如上程式碼所示, 父類中的 sum() 方法對兩個引數進行求和, 子類覆寫了父類的方法並且改變了父類的原有的功能, 子類中的 sum() 方法不再是簡單的進行求和運算。此時我們在父類Parent出現的地方替換為子類Son, 用程式碼描述就是將 new Parent().sum() 替換為 new Son().sum() , 原有的求和運算也被替換成了減法或乘法運算, 很明顯這是兩個不同的業務, 這也就違背了前文中的"所有引用基類的地方必須能透明地使用其子類的物件"這一定義。通常在實際業務中, 如果不得不覆寫父類的方法, 可以有一個通用的做法: 令父類和子類都繼承一個更通用的基類, 採用依賴、聚合、組合等關係代替原有的繼承關係。

子類中可以增加自己特有的方法

此時子類Son擴充套件了一個在父類中並不存在的 study() 方法, 如果我們在實際業務中將子類出現的地方 new Son().study() 替換成父類物件 new Parent().study() 就會產生 java.lang.NoSuchMethodException 異常, 這又進一步驗證了前文中的"如果軟體中使用的是一個子類物件的話,那麼它不一定能夠替換成它的基類物件"這一反面論證。

當子類方法過載父類方法時, 子類方法的前置條件(即方法的形參)要比父類方法的前置條件更寬鬆

public class Parent {
    public void play(Map<String, String> map) {
        System.out.println("父類方法被執行...");
    }
}

public class Son extends Parent {
    public void play(HashMap<String, String> map) {
        System.out.println("子類方法被執行...");
    }
}

public class Demo {
    public static void main(String[] args) {
        HashMap<String, String> paramMap = new HashMap<String, String>();
        new Parent().play(paramMap);//父類方法被執行...
        new Son().play(paramMap);//子類方法被執行...
    }
}

如上, 子類過載父類的方法, 且子類方法的前置條件比父類方法的前置條件更嚴格, 將父類物件替換成子類物件後, 父類原有的方法不再被執行, 業務發生了改變, 這就違背了里氏替換原則的定義。但如果反過來, 子類方法的前置條件相對於父類更寬鬆, 將父類物件替換成子類物件後,  new Son().play(paramMap) 呼叫的仍然是父類方法, 這就符合了"所有引用基類的地方必須能透明地使用其子類的物件"這一定義。

當子類方法實現父類抽象方法時,子類方法的後置條件(即方法的返回值)要比父類方法的後置條件更嚴格

public class Parent {
    public List<String> getHobbys() {
        return new ArrayList<String>();
    }
}

public class Son extends Parent {
    @Override
    public ArrayList<String> getHobbys() {
        return new ArrayList<String>();
    }
}

public class Demo {
    public static void main(String[] args) {
        List<String> hobbys = new Parent().getHobbys();
        //List<String> hobbys = new Son().getHobbys();
    }
}

這個很容易理解, 根據里氏替換原則的定義, 如果子類方法的後置條件比父類更寬鬆, 將父類物件替換成子類物件後, 就會發生編譯異常, 因為子類引用指向了父類物件 ArrayList<String> hobbys = List<String>類引用指向的例項物件 , 顯然連基本的語法都不通過。

綜上所述, 我們在程式中應儘量使用基類型別來對物件進行定義,而在執行時再確定其子類型別,用子類物件來替換基類物件, 另外在使用繼承時遵循里氏替換原則來提高程式碼的健壯性。

依賴倒置原則

依賴倒置原則(Dependency Inversion  Principle, DIP)的定義: 高層模組不應該依賴低層模組, 兩者都應該依賴其抽象; 抽象不應該依賴細節; 細節應該依賴抽象。

依賴倒置原則的核心思想就是"面向介面程式設計", 它是實現開閉原則的一種必要手段。下面通過一段簡單的程式碼示例來說明面向介面程式設計的好處。

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

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

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

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

在上面的高層模組業務類Client中, 司機類和賓士類緊密耦合在一起, 司機只能開賓士車, 如果後期業務發生變更, 需要司機開寶馬車或法拉利, 就需要對司機類進行修改, 其結果就是大大降低了系統的可維護性、可擴充套件性和穩定性, 這顯然也不符合"面向物件設計"中的開閉原則

而且在實際專案中, 經常是多個開發人員協作開發, 可能是A開發人員負責司機類模組, B開發人員負責汽車類模組, 因為司機類依賴汽車類, 所以A開發人員需要等待B開發人員完成汽車類的編寫後才能開展自己的工作, 這就大大的拖延了專案的進度, 即"增加了並行開發引起的風險"。

做過“Web Service"開發的應該知道一個”契約優先“的原則, 就是先定義好"WSDL"介面, 制定好雙方的開發協議, 然後再各自實現。依賴倒置原則也給出了一個相似的規約, 即大家都面向介面程式設計, 不用去關心介面下具體的實現細節。

在上述程式碼示例中, 我們引入依賴倒置原則來對其進行重構, 重構後的UML類圖如下: 

此時司機類不再直接依賴汽車類, 它們都依賴其各自實現的抽象介面, 而司機抽象介面又依賴汽車抽象介面。在高層業務模組中, 不用再關心低層的實現類細節, 基於介面程式設計, 司機的driver()方法形參型別設定為介面或抽象類, 後期如果有業務變動需要司機開寶馬或法拉利, 只需要新增一個寶馬類並在高層業務模組稍作修改即可, 而其他底層模組諸如Driver類則不需要做任何變動。另外在大型專案多人協作開發中, 大家也可以以此來約定好抽象類或介面, 然後各自根據定義好的介面進行具體實現, 就可以大大降低並行開發引起的風險。

在前文中提到, 依賴倒置原則的核心思想就是面向介面程式設計, 即將具體類的物件通過依賴注入的方式注入到其他物件中。依賴注入是指當一個物件要與其他物件發生依賴關係時,通過抽象來注入所依賴的物件。常用的注入方式有三種,分別是:構造注入,Setter方法注入和介面注入。構造注入是指通過建構函式來傳入具體類的物件,設值注入是指通過Setter方法來傳入具體類的物件,而介面注入是指通過在介面中宣告的業務方法來傳入具體類的物件。這些方法在定義時使用的是抽象型別,在執行時再傳入具體型別的物件,由子類物件來覆蓋父類物件。

綜上所述, 採用依賴倒置原則可以減少類間的耦合性, 提高系統的穩定性, 降低並行開發引起的風險, 提高程式碼的可讀性和可維護性。

在實踐中, 依賴倒轉原則要求我們在程式程式碼中傳遞引數時或在關聯關係中,儘量引用層次高的抽象層類,即使用介面和抽象類進行變數型別宣告、引數型別宣告、方法返回型別宣告,以及資料型別的轉換等,而不要用具體類來做這些事情。為了確保該原則的應用,一個具體類應當只實現介面或抽象類中宣告過的方法,而不要給出多餘的方法,否則將無法呼叫到在子類中增加的新方法。

介面隔離原則

介面隔離原則(Interface  Segregation Principle, ISP)的定義: 客戶端不應該依賴它不需要的介面, 類間的依賴關係應該建立在最小的介面上。

下面是引用https://blog.csdn.net/lovelion/article/details/7562842文章中的一段描述: 

根據介面隔離原則,當一個介面太大時,我們需要將它分割成一些更細小的介面,使用該介面的客戶端僅需知道與之相關的方法即可。每一個介面應該承擔一種相對獨立的角色,不幹不該乾的事,該乾的事都要幹。這裡的“介面”往往有兩種不同的含義:一種是指一個型別所具有的方法特徵的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語言具體的“介面”定義,有嚴格的定義和結構,比如Java語言中的interface。對於這兩種不同的含義,ISP的表達方式以及含義都有所不同:

  • 當把“介面”理解成一個型別所提供的所有方法特徵的集合的時候,這就是一種邏輯上的概念,介面的劃分將直接帶來型別的劃分。可以把介面理解成角色,一個介面只能代表一個角色,每個角色都有它特定的一個介面,此時,這個原則可以叫做“角色隔離原則”。
  • 如果把“介面”理解成狹義的特定語言的介面,那麼ISP表達的意思是指介面僅僅提供客戶端需要的行為,客戶端不需要的行為則隱藏起來,應當為客戶端提供儘可能小的單獨的介面,而不要提供大的總介面。在面向物件程式語言中,實現一個介面就需要實現該介面中定義的所有方法,因此大的總介面使用起來不一定很方便,為了使介面的職責單一,需要將大介面中的方法根據其職責不同分別放在不同的小介面中,以確保每個介面使用起來都較為方便,並都承擔某一單一角色。介面應該儘量細化,同時介面中的方法應該儘量少,每個介面中只包含一個客戶端(如子模組或業務邏輯類)所需的方法即可,這種機制也稱為“定製服務”,即為不同的客戶端提供寬窄不同的介面。

下面通過一個簡單的示例來說明介面隔離原則: 

上圖這個設計未遵循介面隔離原則, 類A依賴介面I中的方法1、方法2、方法3,類B是對類A依賴的實現。類C依賴介面I中的方法1、方法4、方法5,類D是對類C依賴的實現。對於類B和類D來說,雖然他們都存在著用不到的方法(也就是圖中紅色字型標記的方法),但由於實現了介面I,所以也必須要實現這些用不到的方法。下面根據介面隔離原則, 對介面I進行拆分, 拆分後的類圖如下。

將介面I拆分成三個介面後, 類A和類C只能看到它所需要的方法, 同時拆分後的介面只專注為一個模組提供定製服務, 實現類也不用再實現和他職責無關的方法。

綜上所述, 在使用介面隔離原則時,我們需要注意控制介面的粒度,介面不能太小,如果太小會導致系統中介面氾濫,不利於維護;介面也不能太大,太大的介面將違背介面隔離原則,靈活性較差,使用起來很不方便。一般而言,介面中僅包含為某一類使用者定製的方法即可,不應該強迫客戶依賴於那些它們不用的方法。

迪米特法則

原文連結: https://blog.csdn.net/lovelion/article/details/7563445

迪米特法則(Law of  Demeter, LoD)也稱為最少知識原則, 通俗點來講, 就是一個軟體實體應當儘可能少地與其他實體發生相互作用

如果一個系統符合迪米特法則,那麼當其中某一個模組發生修改時,就會盡量少地影響其他模組,擴充套件會相對容易,這是對軟體實體之間通訊的限制,迪米特法則要求限制軟體實體之間通訊的寬度和深度。迪米特法則可降低系統的耦合度,使類與類之間保持鬆散的耦合關係。

迪米特法則還有幾種定義形式,包括不要和“陌生人”說話只與你的直接朋友通訊等,在迪米特法則中,對於一個物件,其朋友包括以下幾類:

  1. 當前物件本身(this);
  2. 以引數形式傳入到當前物件方法中的物件;
  3. 當前物件的成員物件;
  4. 如果當前物件的成員物件是一個集合,那麼集合中的元素也都是朋友;
  5. 當前物件所建立的物件。

任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”,否則就是“陌生人”。在應用迪米特法則時,一個物件只能與直接朋友發生互動,不要與“陌生人”發生直接互動,這樣做可以降低系統的耦合度,一個物件的改變不會給太多其他物件帶來影響。

迪米特法則要求我們在設計系統時,應該儘量減少物件之間的互動,如果兩個物件之間不必彼此直接通訊,那麼這兩個物件就不應當發生任何直接的相互作用,如果其中的一個物件需要呼叫另一個物件的某一個方法的話,可以通過第三者轉發這個呼叫。簡言之,就是通過引入一個合理的第三者來降低現有物件之間的耦合度

在將迪米特法則運用到系統設計中時,要注意下面的幾點:在類的劃分上,應當儘量建立鬆耦合的類,類之間的耦合度越低,就越有利於複用,一個處在鬆耦合中的類一旦被修改,不會對關聯的類造成太大波及在類的結構設計上,每一個類都應當儘量降低其成員變數和成員函式的訪問許可權在類的設計上,只要有可能,一個型別應當設計成不變類在對其他類的引用上,一個物件對其他物件的引用應當降到最低

下面通過一個簡單例項來加深對迪米特法則的理解: 

Sunny軟體公司所開發CRM系統包含很多業務操作視窗,在這些視窗中,某些介面控制元件之間存在複雜的互動關係,一個控制元件事件的觸發將導致多個其他介面控制元件產生響應,例如,當一個按鈕(Button)被單擊時,對應的列表框(List)、組合框(ComboBox)、文字框(TextBox)、文字標籤(Label)等都將發生改變,在初始設計方案中,介面控制元件之間的互動關係可簡化為如圖1所示結構:

                                                                                    圖1 初始設計方案結構圖

在圖1中,由於介面控制元件之間的互動關係複雜,導致在該視窗中增加新的介面控制元件時需要修改與之互動的其他控制元件的原始碼,系統擴充套件性較差,也不便於增加和刪除新控制元件。

現使用迪米特對其進行重構。

在本例項中,可以通過引入一個專門用於控制介面控制元件互動的中間類(Mediator)來降低介面控制元件之間的耦合度。引入中間類之後,介面控制元件之間不再發生直接引用,而是將請求先轉發給中間類,再由中間類來完成對其他控制元件的呼叫。當需要增加或刪除新的控制元件時,只需修改中間類即可,無須修改新增控制元件或已有控制元件的原始碼,重構後結構如圖2所示: 

開閉原則

開閉原則的定義(Open-Closed Principle, OCP): 一個軟體實體如類、模組和函式應該對擴充套件開放, 對修改關閉。通俗點講, 就是一個軟體實體應該通過擴充套件來實現變化, 而不是通過修改已有的程式碼來實現變化。

在一個軟體的生命週期內, 業務需求發生變化是一個常態, 這就要求我們在系統設計時要主動擁抱變化, 而開閉原則就是為軟體實體的未來變更而制定的對現行開發設計進行約束的一個規則。

在前文中, 我們已介紹過其他5個原則,它們是指導設計的工具和方法, 而開閉原則是目標。單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向介面程式設計;介面隔離原則告訴我們在設計介面的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴充套件開放,對修改關閉。

那麼我們在實踐中怎麼運用這6大設計原則呢?這裡有一篇文章對此闡述的非常形象生動, 就轉載過來了: https://blog.csdn.net/zhengzhb/article/details/7296944

對這六個原則的遵守並不是是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是一樣,制定這六個原則的目的並不是要我們刻板的遵守他們,而需要根據實際情況靈活運用。對他們的遵守程度只要在一個合理的範圍內,就算是良好的設計。我們用一幅圖來說明一下。

 圖中的每一條維度各代表一項原則,我們依據對這項原則的遵守程度在維度上畫一個點,則如果對這項原則遵守的合理的話,這個點應該落在紅色的同心圓內部;如果遵守的差,點將會在小圓內部;如果過度遵守,點將會落在大圓外部。一個良好的設計體現在圖中,應該是六個頂點都在同心圓中的六邊形。

在上圖中,設計1、設計2屬於良好的設計,他們對六項原則的遵守程度都在合理的範圍內;設計3、設計4設計雖然有些不足,但也基本可以接受;設計5則嚴重不足,對各項原則都沒有很好的遵守;而設計6則遵守過渡了,設計5和設計6都是迫切需要重構的設計。

參考資料

設計模式系列一

設計模式系列二

設計模式之六大原則(轉載)

<<設計模式之禪>>