1. 程式人生 > >設計模式--六大設計原則

設計模式--六大設計原則

模式 png 們的 正常的 方法 res 約束 軟件架構 其他

1.單一職責原則(SRP )

定義:Single Responsibility Principle

?   不要存在多於一個導致類變更的原因。通俗的說,即一個類只負責一項職責。

問題由來:

?   類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。所以記住,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構。)

  遵循單一職責原的優點有:

?   可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;? 提高類的可讀性,提高系統的可維護性;? 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。

?   需要說明的一點是單一職責原則不只是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用單一職責原則。

基本使用原則(個人非官方):只有邏輯足夠簡單,才可以在代碼級別(類級別)上違反單一職責原則;只有類中方法數量足夠少,才可以在方法級別上違反單一職責原則

2.裏氏替換原則(LSP)

Liskov Substitution Principle

定義1:

  如果對每一個類型為 T1的對象 o1,都有類型為 T2 的對象o2,使得以 T1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時,程序 P 的行為沒有發生變化,那麽類型 T2 是類型 T1 的子類型。

?定義2:

  所有引用基類的地方必須能透明地使用其子類的對象

?問題由來:

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

?解決方案:

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

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

?   繼承作為面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能會產生故障。

?   如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合,組合等關系代替。

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

? 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。? 子類中可以增加自己特有的方法。? 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。? 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。(eg:父類抽象方法通常可以返回抽象類的對象,子類實現時候需要返回具有的實現類)? 看上去很不可思議,因為我們會發現在自己編程中常常會違反裏氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循裏氏替換原則會有什麽後果? 後果就是:你寫的代碼出問題的幾率將會大大增加。

3.依賴倒置原則(DIP)

Dependence Inversion Principle

定義:

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

問題由來:

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

解決方案:

  將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。

依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。

依賴倒置原則的核心思想是面向接口編程

傳遞依賴關系有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。在實際編程中,我們一般需要做到如下3點:

低層模塊盡量都要有抽象類或接口,或者兩者都有。變量的聲明類型盡量是抽象類或接口。使用繼承時遵循裏氏替換原則。

依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。

4.接口隔離原則(ISP)

Interface Segregation Principle

定義:

  客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。

問題由來:

  類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對於類A和類B來說不是最小接口,則類B和類D必須去實現他們不需要的方法。

解決方案:

  將臃腫的接口I拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關系。也就是采用接口隔離原則。

舉例來說明接口隔離原則:

技術分享圖片

技術分享圖片

?           (圖1 未遵循接口隔離原則的設計)

? 這個圖的意思是:類A依賴接口I中的方法1、方法2、方法3,類B是對類A依賴的實現。類C依賴接口I中的方法1、方法4、方法5,類D是對類C依賴的實現。對於類B和類D來說,雖然他們都存在著用不到的方法(也就是圖中紅色字體標記的方法),但由於實現了接口I,所以也必須要實現這些用不到的方法。

接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。本文例子中,將一個龐大的接口變更為3個專用的接口所采用的就是接口隔離原則。在程序設計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設計時對外部設定的“契約”,通過分散定義多個接口,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

? 說到這裏,很多人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。其一,單一職責原則原註重的是職責;而接口隔離原則註重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口,主要針對抽象,針對程序整體框架的構建。

? 采用接口隔離原則對接口進行約束時,要註意以下幾點:

? 接口盡量小,但是要有限度。對接口進行細化可以提高程序設計靈活性是不掙的事實,但是如果過小,則會造成接口數量過多,使設計復雜化。所以一定要適度。? 為依賴接口的類定制服務,只暴露給調用的類它需要的方法,它不需要的方法則隱藏起來。只有專註地為一個模塊提供定制服務,才能建立最小的依賴關系。? 提高內聚,減少對外交互。使接口用最少的方法去完成最多的事情。

? 運用接口隔離原則,一定要適度,接口設計的過大或過小都不好。設計接口的時候,只有多花些時間去思考和籌劃,才能準確地實踐這一原則。

5.迪米特法則(LOD)/最少知識原則(LKP)

Law of Demeter / Least Knowledge Principle

定義:

  一個對象應該對其他對象保持最少的了解。

問題由來:

  類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

解決方案:

  盡量降低類與類之間的耦合

自從我們接觸編程開始,就知道了軟件編程的總的原則:低耦合,高內聚。無論是面向過程編程還是面向對象編程,只有使各個模塊之間的耦合盡量的低,才能提高代碼的復用率。低耦合的優點不言而喻,但是怎麽樣編程才能做到低耦合呢?那正是迪米特法則要去完成的。

? 迪米特法則又叫最少知識原則,最早是在1987年由美國Northeastern University的Ian Holland提出。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類來說,無論邏輯多麽復雜,都盡量地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外泄漏任何信息。迪米特法則還有一個更簡單的定義:只與直接的朋友通信。首先來解釋一下什麽是直接的朋友:每個對象都會與其他對象有耦合關系,只要兩個對象之間有耦合關系,我們就說這兩個對象之間是朋友關系。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變量、方法參數、方法返回值中的類為直接的朋友,而出現在局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要作為局部變量的形式出現在類的內部。

? 迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關系。但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發生聯系。過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統復雜度變大。所以在采用迪米特法則時要反復權衡,既做到結構清晰,又要高內聚低耦合。

6.開閉原則(OCP)

Open - ClosedPrinciple

定義:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。問題由來:在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。

解決方案:當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。(重視,避免重構,功能的重測,最常見的開發中口頭體現:修該、新增了xx功能一定不要影響原有的功能。)

? 開閉原則是面向對象設計中最基礎的設計原則,它指導我們如何建立穩定靈活的系統。開閉原則可能是設計模式六項原則中定義最模糊的一個了,它只告訴我們對擴展開放,對修改關閉,可是到底如何才能做到對擴展開放,對修改關閉,並沒有明確的告訴我們。以前,如果有人告訴我“你進行設計的時候一定要遵守開閉原則”,我會覺的他什麽都沒說,但貌似又什麽都說了。因為開閉原則真的太虛了。

在仔細思考以及仔細閱讀很多設計模式的文章後,終於對開閉原則有了一點認識。其實,我們遵循設計模式前面5大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要我們對前面5項原則遵守的好了,設計出的軟件自然是符合開閉原則的,這個開閉原則更像是前面五項原則遵守程度的“平均得分”,前面5項原則遵守的好,平均分自然就高,說明軟件設計開閉原則遵守的好;如果前面5項原則遵守的不好,則說明開閉原則遵守的不好。

開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。而軟件中易變的細節,我們用從抽象派生的實現類來進行擴展,當軟件需要發生變化時,我們只需要根據需求重新派生一個實現類來擴展就可以了。當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性才行。

說到這裏,再回想一下前面說的5項原則,恰恰是告訴我們用抽象構建框架,用實現擴展細節的註意事項而已:單一職責原則告訴我們實現類要職責單一;裏氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴展開放,對修改關閉。

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

技術分享圖片

技術分享圖片

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

技術分享圖片

技術分享圖片

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

設計模式--六大設計原則