萬變不離其宗,不管是Java還是C++,凡是面向對象的編程語言,在設計上,儘管表現形式可能有所不同,但是其實質和所需遵守的原則都是一致的。本文便是帶領讀者去深入理解設計模式中的六大原則,以期幫助讀者做出更好的設計。 ...
深入理解設計模式六大原則
萬變不離其宗,不管是Java還是C++,凡是面向對象的編程語言,在設計上,儘管表現形式可能有所不同,但是其實質和所需遵守的原則都是一致的。本文便是帶領讀者去深入理解設計模式中的六大原則,以期幫助讀者做出更好的設計。
單一職責原則
單一職責原則:Single Responsibility Principle,簡稱SRP
定義:
應該有且僅有一個原因引起類的變更。
問題場景:
類C負責兩個不同的職責:職責D1,職責D2。當由於職責D1需求發生改變而需要修改類C時,有可能會導致原本運行正常的職責D2功能發生故障。
單一職責最難劃分的就是職責,一個職責一個介面,但問題是”職責“沒有一個量化的標準,一個類到底要負責哪些職責?這些職責怎麼細化?細化後是否都要有一個介面或類?這些都需要從實際的項目去考慮。
解決方案:
遵循單一職責原則。分別建立兩個類C1、C2,使C1完成職責D1功能,C2完成職責D2功能。這樣,當修改類C1時,不會使職責D2發生故障風險;同理,當修改C2時,也不會使職責D1發生故障風險。
比如說一個用戶類,應該把用戶的信息抽取成一個BO(Business Object,業務對象),把行為抽取成一個Biz(Business Logic,業務邏輯)。這樣前者的職責是收集和反饋用戶的屬性信息;後者的職責是完成用戶信息的維護和變更。分成這樣的兩個介面來設計之後,這兩個職責的變化就不會互相影響。
單一職責的好處:
- 類的複雜性降低,實現什麼職責都有清晰明確的定義;
- 可讀性提高;
- 可維護性提高;
- 變更引起的風險降低。
變更是必不可少的,如果介面的的單一職責做得好,一個介面修改只對相應的實現類有影響,對其他的介面無影響,這對系統的擴展性、維護性都有非常大的幫助。
里氏替換原則
里氏替換原則:Liskov Substitution Principle,簡稱LSP。這一原則最早在1988年,由麻省理工學院的一位叫做Barbara Liskov提出來的,所以將其命名為里氏替換原則。
定義一(標准定義):
如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程式P在所有的對象o1都代換成o2時,程式P的行為沒有發生變化,那麼類型S是類型T的子類型。
定義二(通俗定義):
所有引用基類的地方必須能透明地使用其子類的對象。
從定義二中可以理解到,只要父類能出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者可能根本不需要知道是父類還是子類。但反之就不行了。
里氏替換原則的規範:
-
子類必須完全實現父類的方法
- 在類中調用其他類時務必要使用父類或介面,如果不能使用父類或介面,則說明類的設計已經違背了LSP原則。
- 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生”畸變“,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係代替繼承。
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
-
子類可以有自己的個性
子類中可以增加自己特有的方法。因為子類可能有比父類多的屬性和行為,所以向下轉型是不安全的,從LSP來看,就是有子類出現的地方父類未必就可以出現。
-
覆蓋或實現父類的方法時參數可以被放大
LSP要求制定一個契約,就是父類或介面,這種設計方法也叫做Design by Contract。契約制定了,也就同時制定了前置條件(即方法的形參)和後置條件(即方法的返回值)。
在實際應用中父類一般都是抽象類,子類是實現類,子類中方法的前置條件必須與超類中被覆寫的方法的前置條件相同或者更寬鬆。
-
覆寫或實現父類的方法時輸出結構可以被縮小
父類的一個方法的返回值是一個類型T,子類的相同方法(重載或覆寫)的返回值為S,那麼LSP就要求S必須小於或等於T。
依賴倒置原則
定義:
高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。
問題場景:
類A直接依賴於類B,假如要將類A改為依賴於類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,類B和類C是低層模塊,假如修改了類A,可能會給程式帶來不必要的風險。
解決方案:
將類A修改為依賴介面I,類B和類C各自實現介面I,類A通過介面I間接與類或者類C發生聯繫,則會大大降低修改類A的幾率。
依賴倒置原則的核心思想是面向介面編程。
依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是介面或者抽象類,細節就是具體的實現類,使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
依賴的三種寫法:
依賴是可以傳遞的,對象的依賴關係有三種方式來傳遞:
- 構造函數傳遞依賴對象。在類中通過構造函數聲明依賴對象,按照依賴註入的說法,這種方式叫作構造函數註入;
- Setter方法傳遞依賴對象。在抽象類中設置Setter方法聲明依賴關係,依照依賴註入的說法,這是Setter依賴註入;
- 介面聲明依賴對象。在介面的方法中聲明依賴對象,這種方法也叫做介面註入。
最佳實踐:
依賴倒置原則的本質就是通過抽象(介面或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合。在實際項目中,如何應用這個規則呢,只要遵循以下幾個規則就可以:
- 每個類儘量都有介面或抽象類,或者抽象類和介面兩者都具備,這是依賴倒置的基本要求,有了抽象才可能依賴倒置;
- 變數的錶面類型儘量是介面或者是抽象類;
- 任何類都不應該從具體類派生;
- 儘量不要覆寫基類的方法;
- 結合里氏替換原則使用:介面負責定義public屬性和方法,並且聲明與其他對象的依賴關係,抽象類負責公共構造部分的實現,實現類準確的實現業務邏輯,同時在適當的時候對父類進行細化。
介面隔離原則
定義:
客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。
介面分為兩種:
- 實例介面:在Java中聲明一個類,然後用new關鍵字產生一個實例,它是對一個類型的事物的描述,這是一種介面,從這個角度來看,Java中的類也是一種介面;
- 類介面:Java中經常使用的interface關鍵字定義的介面。
什麼是隔離呢?它有兩種定義:
- 客戶端不應該依賴它不需要的介面;
- 類間的依賴關係應該建立在最小的介面上。
這兩句話可以概括為一句話:建立單一介面,不要建立臃腫龐大的介面。更通俗的講:介面儘量細化,同時介面中的方法儘量少。
問題由來:
類A通過介面I依賴類B,類C通過介面I依賴類D,如果介面I對於類A和類B來說不是最小介面,則類B和類D必須去實現他們不需要的方法。
解決方案:
將臃腫的介面I拆分為獨立的幾個介面,類A和類C分別與他們需要的介面建立依賴關係。也就是採用介面隔離原則。
介面隔離原則 vs. 單一職責原則:
二者的審視角度不同,單一職責要求的是類和介面職責單一,註重的是職責,這是業務邏輯上的劃分,而介面隔離原則要求介面的方法儘量少,它要求”儘量使用多個專門的介面“。
最佳實踐:
- 介面要儘量小,一個介面只服務於一個子模塊或業務邏輯,根據介面隔離原則拆分介面時,首先必須滿足單一職責原則;
- 介面要高內聚,具體來講就是在介面中儘量少公佈public方法,介面是對外的承諾,承諾越少對系統的開發越有利,變更的風險也就越少,同時也有利於降低成本;
- 已經被污染了的介面,儘量去修改,若變更的風險較大,則採用適配器模式進行轉化處理;
- 定製服務:定製服務是單獨為一個個體提供優良的服務,要求是只提供訪問者需要的方法;
- 介面設計是有限度的:介面設計的粒度需要根據經驗和常識進行合理的判斷。
迪米特法則
定義:
Law of Demeter,簡稱LoD,也稱為最少知識原則,Least Knowledge Principle,簡稱LKP,兩個名字含義相同:一個對象應該對其他對象有最少的瞭解,即一個類應該對自己需要耦合或調用的類知道得最少,只關註自己調用的public方法,其他的一概不關心。
問題由來:
類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
最佳實踐:
迪米特法則的核心思想就是類間解耦,弱耦合,只有弱耦合了以後,類的復用率才可以提高。其要求的結果就是產生了大量的中轉或跳轉類,導致系統的複雜性提高,同時也為維護帶來了的難度。因此在採用迪米特法則時,既要做到讓結構清晰,又做到高內聚低耦合。
開閉原則
定義:
一個軟體實體類,如類、模塊和函數應該對擴展開放,對修改關閉。
問題由來:
在軟體的生命周期內,因為變化、升級和維護等原因需要對軟體原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。
最佳實踐:
開閉原則是一個非常虛的原則,前面5個原則是對開閉原則的具體解釋,但是開閉原則並不局限於這麼多。在實際工作中需要註意以下幾點:
- 抽象約束:抽象是對一組事物的通用描述,沒有具體的實現,也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此介面或抽象類可以約束一組可能變化的行為,並且能夠實現對擴展開放。
- 元數據控制模塊行為:元數據是用來描述環境和數據的數據,通俗地講就是配置參數,參數可以從文件中獲得,也可以從資料庫中獲得,使用此方法的極致就是控制反轉,使用最多的就是Spring容器。
- 制定項目章程:對項目來說,約定優於配置。章程中指定了所有人員都必須遵守的約定。
- 封裝變化:將相同的變化封裝到一個介面或抽象類中;將不同的變化封裝到不同的介面或抽象類中,不應該有兩個不同的變化出現在同一個介面或抽象類中。封裝變化,也就是受保護的變化,找出預計有變化或不穩定的點,為這些變化點創建穩定的介面。
六大設計原則應用
理解:
從整體上來理解六大設計原則,可以簡要的概括為一句話,用抽象構建框架,用實現擴展細節,具體到每一條設計原則,則對應一條註意事項:
- 單一職責原則告訴我們實現類要職責單一;
- 里氏替換原則告訴我們不要破壞繼承體系;
- 依賴倒置原則告訴我們要面向介面編程;
- 介面隔離原則告訴我們在設計介面的時候要精簡單一;
- 迪米特法則告訴我們要降低耦合;
- 開閉原則是總綱,告訴我們要對擴展開放,對修改關閉。
遵守:
理解了這六大設計原則之後,如何來遵守呢?制定這六條原則的目的並不是要我們刻板的遵守,而是根據實際需要靈活運用。只要對它們的遵守程度在一個合理的範圍內,就算是良好的設計,用一幅圖來說明一下:
圖中的每一條維度各代表一項原則,我們依據對這項原則的遵守程度在維度上畫一個點,則如果對這項原則遵守的合理的話,這個點應該落在紅色的同心圓內部;如果遵守的差,點將會在小圓內部;如果過度遵守,點將會落在大圓外部。一個良好的設計體現在圖中,應該是六個頂點都在同心圓中的六邊形。
在上圖中,設計1、設計2屬於良好的設計,他們對六項原則的遵守程度都在合理的範圍內;設計3、設計4設計雖然有些不足,但也基本可以接受;設計5則嚴重不足,對各項原則都沒有很好的遵守;而設計6則遵守過渡了,設計5和設計6都是迫切需要重構的設計。
關註我的公眾號,獲取更多關於面試、技術的文章及福利資源。
【參考資料】
《設計模式之禪》
《大話設計模式》