面向對象設計原則

1) 單一職責原則 (The Single Responsiblity Principle,簡稱SRP)

2) 開放-封閉原則 (The Open-Close Principle,簡稱OCP)

3) Liskov 替換原則(The Liskov Substitution Principle,簡稱LSP)

4) 依賴倒置原則(The Dependency Inversion Pricinple,簡稱DIP)

5) 接口隔離原則 (The Interface Segregation Principle,簡稱ISP)

一、單一職責原則(SRP)

就一個類而言,應該僅有一個引起它變化的原因。軟件設計真正要做的許多內容,就是發現職責並把那些職責相互分離。測試驅動的開發實踐常常會在設計出現臭味之前就迫使我們分離職責。

二、開閉原則(OCP)

軟件實體(類、模塊、函數)應該是可擴展的,但是不可修改的。也就是說:對於擴展是開放的,對於更改是封閉的。怎樣可能在不改動模塊源代碼的情況下 去更改它的行爲呢?怎樣才能在無需對模塊進行改動的情況下就改變它的功能呢?關鍵是抽象!因此在進行面向對象設計時要儘量考慮接口封裝機制、抽象機制和多 態技術。該原則同樣適合於非面向對象設計的方法,是軟件工程設計方法的重要原則之一。

三、替換原則(LSP)

子類應當可以替換父類並出現在父類能夠出現的任何地方。這個原則是Liskov於1987年提出的設計原則。它同樣可以從Bertrand Meyer 的DBC (Design by Contract〔基於契約設計〕) 的概念推出。

四、依賴倒置原則(DIP)

1、高層模塊不應該依賴於低層模塊。二者都應該依賴於抽象。2、抽象不應該依賴於細節。細節應該依賴於抽象。在進行業務設計時,與特定業務有關的依 賴關係應該儘量依賴接口和抽象類,而不是依賴於具體類。具體類只負責相關業務的實現,修改具體類不影響與特定業務有關的依賴關係。在結構化設計中,我們可 以看到底層的模塊是對高層抽象模塊的實現(高層抽象模塊通過調用底層模塊),這說明,抽象的模塊要依賴具體實現相關的模塊,底層模塊的具體實現發生變動時 將會嚴重影響高層抽象的模塊,顯然這是結構化方法的一個"硬傷"。面向對象方法的依賴關係剛好相反,具體實現類依賴於抽象類和接口。

五、接口分離原則(ISP)

採用多個與特定客戶類有關的接口比採用一個通用的涵蓋多個業務方法的接口要好。  ISP原則是另外一個支持諸如COM等組件化的使能技術。缺少 ISP,組件、類的可用性和移植性將大打折扣。這個原則的本質相當簡單。如果你擁有一個針對多個客戶的類,爲每一個客戶創建特定業務接口,然後使該客戶類 繼承多個特定業務接口將比直接加載客戶所需所有方法有效。

以上五個原則是面向對象中常常用到的原則。此外,除上述五原則外,還有一些常用的經驗諸如類結構層次以三到四層爲宜、類的職責明確化(一個類對應一個具體職責)等可供我們在進行面向對象設計參考。 但就上面的幾個原則看來,我們看到這些類在幾何分佈上呈現樹型拓撲的關係,這是一種良好、開放式的線性關係、具有較低的設計複雜度。一般說來,在軟件設計中我們應當儘量避免出現帶有閉包、循環的設計關係,它們反映的是較大的耦合度和設計複雜化。

  1. 單一職責原則(SRP):一個類應當只有一個改變的原因,類只需要知道一件事情,它們應當有一個單獨的職責,要點就是當一個類需要改變時,應當只有一個原因。
  2. 開放-封閉原則(OCP):軟件實體(類、模塊、函數等)應當爲擴展而開放,又爲修改而封閉。這個原則有一個相當詳細的定義,但是一個簡單的意思是:你應當能夠改變一個模塊的周邊環境而無須改變模塊本身。
  3. Liskov替換原則(LSP):子類型(subtypes)必須是爲它們的基類型(base types)可替代的。
  4. 依存關係倒置原則(DIP) :A.高層模塊應當不依賴低層模塊,它們應當依賴於抽象。
    B.抽象應當不依賴於細節,細節應當依賴於抽象。
    更好的描述是:不要依賴那些容易變化的具體類。如果你要繼承一個類,從一個抽象類繼承吧。如果你要持有一個類的引用,從一個抽象的類引用吧。如果你要調用一個函數,從一個抽象的函數調用吧。
  5. 接口隔離原則(ISP):客戶不應當依賴那些它們根本不用的方法。

總結:
五個簡單的原則是:
1、SRP--一個類應當只有一個發生變化的原因。 
2、OCP――應當能夠改變一個類的環境,而無須改變類本身。
3、LSP――避免造成派生類的方法非法或退化,一個基類的用戶應當不需要知道這個派生類。
4、DIP ――用依賴於接口和抽象類來替代依賴容易變化的具體類。
5、ISP――給一個對象的每一個用戶一個接口,這個接口僅有用戶需要的方法。


正 如牛頓三大定律在經典力學中的位置一樣,“開-閉”原則(Open-Closed Principle)是面向對象的可複用設計(Object Oriented Design或OOD)的基石。其他設計原則(里氏代換原則、依賴倒轉原則、合成/聚合複用原則、迪米特法則、接口隔離原則)是實現“開-閉”原則的手段 和工具。
  一、“開-閉”原則(Open-Closed Principle,OCP)
   1.1“開-閉”原則的定義及優點
   1)定義:一個軟件實體應當對擴展開放,對修改關閉( Software entities should be open for extension,but closed for modification.)。即在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展。
  2)滿足“開-閉”原則的系統的優點
  a)通過擴展已有的軟件系統,可以提供新的行爲,以滿足對軟件的新需求,使變化中的軟件系統有一定的適應性和靈活性。
  b)已有的軟件模塊,特別是最重要的抽象層模塊不能再修改,這就使變化中的軟件系統有一定的穩定性和延續性。
  c)這樣的系統同時滿足了可複用性與可維護性。
  1.2如何實現“開-閉”原則
  在面向對象設計中,不允許更改的是系統的抽象層,而允許擴展的是系統的實現層。換言之,定義一個一勞永逸的抽象設計層,允許儘可能多的行爲在實現層被實現。
  解決問題關鍵在於抽象化,抽象化是面向對象設計的第一個核心本質。 
  對一個事物抽象化,實質上是在概括歸納總結它的本質。抽象讓我們抓住最最重要的東西,從更高一層去思考。這降低了思考的複雜度,我們不用同時考慮那麼多的東西。換言之,我們封裝了事物的本質,看不到任何細節。
  在面向對象編程中,通過抽象類及接口,規定了具體類的特徵作爲抽象層,相對穩定,不需更改,從而滿足“對修改關閉”;而從抽象類導出的具體類可以改變系統的行爲,從而滿足“對擴展開放”。[Page]
  對實體進行擴展時,不必改動軟件的源代碼或者二進制代碼。關鍵在於抽象。
  1.3對可變性的封裝原則
   “開-閉”原則也就是“對可變性的封裝原則”(Principle of Encapsulation of Variation ,EVP)。即找到一個系統的可變因素,將之封裝起來。換言之,在你的設計中什麼可能會發生變化,應使之成爲抽象層而封裝,而不是什麼會導致設計改變才封 裝。
   “對可變性的封裝原則”意味着:
  a)一種可變性不應當散落在代碼的許多角落,而應當被封裝到一個對象裏面。同一可變性的不同表象意味着同一個繼承等級結構中的具體子類。因此,此處可以期待繼承關係的出現。繼承是封裝變化的方法,而不僅僅是從一般的對象生成特殊的對象。
  b)一種可變性不應當與另一種可變性混合在一起。作者認爲類圖的繼承結構如果超過兩層,很可能意味着兩種不同的可變性混合在了一起。
  使用“可變性封裝原則”來進行設計可以使系統遵守“開-閉”原則。
  即使無法百分之百的做到“開-閉”原則,但朝這個方向努力,可以顯著改善一個系統的結構。
  二、里氏代換原則(Liskov Substitution Principle, LSP)
  2.1概念
  定義:如果對每一個類型爲T1的對象O1,都有類型爲T2 的對象O2,使得以T1定義的所有程序P在所有的對象O1都代換爲O2時,程序P的行爲沒有變化,那麼類型T2是類型T1的子類型。
  即,一個軟件實體如果使用的是一個基類的話,那麼一定適用於其子類。而且它覺察不出基類對象和子類對象的區別。也就是說,在軟件裏面,把基類都替換成它的子類,程序的行爲沒有變化。
  反過來的代換不成立,如果一個軟件實體使用的是一個子類的話,那麼它不一定適用於基類。
  任何基類可以出現的地方,子類一定可以出現。
  基於契約的設計、抽象出公共部分作爲抽象基類的設計。
  2.2里氏代換原則與“開-閉”原則的關係
   實現“開-閉”原則的關鍵步驟是抽象化。基類與子類之間的繼承關係就是抽象化的體現。因此里氏代換原則是對實現抽象化的具體步驟的規範。
   違反里氏代換原則意味着違反了“開-閉”原則,反之未必。
  三、 依賴倒轉原則(dependence inversion principle, DIP)
   3.1概念
    依賴倒轉原則就是要依賴於抽象,不要依賴於實現。(Abstractions should not depend upon details. Details should depend upon abstractions.)要針對接口編程,不要針對實現編程。(Program to an interface, not an implementation.)
  也就是說應當使用接口和抽象類進行變量類型聲明、參數類型聲 明、方法返還類型說明,以及數據類型的轉換等。而不要用具體類進行變量的類型聲明、參數類型聲明、方法返還類型說明,以及數據類型的轉換等。要保證做到這 一點,一個具體類應當只實現接口和抽象類中聲明過的方法,而不要給出多餘的方法。
  傳統的過程性系統的設計辦法傾向於使高層次的模塊依賴於低層次的模塊,抽象層次依賴於具體層次。倒轉原則就是把這個錯誤的依賴關係倒轉過來。

  面向對象設計的重要原則是創建抽象化,並且從抽象化導出具體化,具體化給出不同的實現。繼承關係就是一種從抽象化到具體化的導出。[Page]
  抽象層包含的應該是應用系統的商務邏輯和宏觀的、對整個系統來說重要的戰略性決定,是必然性的體現。具體層次含有的是一些次要的與實現有關的算法和邏輯,以及戰術性的決定,帶有相當大的偶然性選擇。具體層次的代碼是經常變動的,不能避免出現錯誤。
  從複用的角度來說,高層次的模塊是應當複用的,而且是複用的重點,因爲它含有一個應用系統最重要的宏觀商務邏輯,是較爲穩定的。而在傳統的過程性設計中,複用則側重於具體層次模塊的複用。
  依賴倒轉原則則是對傳統的過程性設計方法的“倒轉”,是高層次模塊複用及其可維護性的有效規範。
  特例:對象的創建過程是違背“開—閉”原則以及依賴倒轉原則的,但通過工廠模式,能很好地解決對象創建過程中的依賴倒轉問題。
  3.2關係
  “開-閉”原則與依賴倒轉原則是目標和手段的關係。如果說開閉原則是目標,依賴倒轉原則是到達\"開閉\"原則的手段。如果要達到最好的\"開閉\"原則,就要儘量的遵守依賴倒轉原則,依賴倒轉原則是對\"抽象化\"的最好規範。
  里氏代換原則是依賴倒轉原則的基礎,依賴倒轉原則是里氏代換原則的重要補充。
  3.3耦合(或者依賴)關係的種類:
  零耦合(Nil Coupling)關係:兩個類沒有耦合關係
  具體耦合(Concrete Coupling)關係:發生在兩個具體的(可實例化的)類之間,經由一個類對另一個具體類的直接引用造成。
  抽象耦合(Abstract Coupling)關係:發生在一個具體類和一個抽象類(或接口)之間,使兩個必須發生關係的類之間存有最大的靈活性。
  3.3.1如何把握耦合
  我們應該儘可能的避免實現繼承,原因如下:
  1 失去靈活性,使用具體類會給底層的修改帶來麻煩。
   2 耦合問題,耦合是指兩個實體相互依賴於對方的一個量度。程序員每天都在(有意識地或者無意識地)做出影響耦合的決定:類耦合、API耦合、應用程序耦合等 等。在一個用擴展的繼承實現系統中,派生類是非常緊密的與基類耦合,而且這種緊密的連接可能是被不期望的。如B extends A ,當B不全用A中的所有methods時,這時候,B調用的方法可能會產生錯誤!
  我們必須客觀的評價耦合度,系統之間不可能總是鬆耦合的,那樣肯定什麼也做不了。
  3.3.2我們決定耦合的程度的依據何在呢?
   簡單的說,就是根據需求的穩定性,來決定耦合的程度。對於穩定性高的需求,不容易發生變化的需求,我們完全可以把各類設計成緊耦合的(我們雖然討論類之 間的耦合度,但其實功能塊、模塊、包之間的耦合度也是一樣的),因爲這樣可以提高效率,而且我們還可以使用一些更好的技術來提高效率或簡化代碼,例如c# 中的內部類技術。可是,如果需求極有可能變化,我們就需要充分的考慮類之間的耦合問題,我們可以想出各種各樣的辦法來降低耦合程度,但是歸納起來,不外乎 增加抽象的層次來隔離不同的類,這個抽象層次可以是抽象的類、具體的類,也可以是接口,或是一組的類。我們可以用一句話來概括降低耦合度的思想:\"針對 接口編程,而不是針對實現編程。
  在我們進行編碼的時候,都會留下我們的指紋,如public的多少,代碼的格式等等。我們可以耦合度量評估重 新構建代碼的風險。因爲重新構建實際上是維護編碼的一種形式,維護中遇到的那些麻煩事在重新構建時同樣會遇到。我們知道在重新構建之後,最常見的隨機 bug大部分都是不當耦合造成的 。[Page]
  如果不穩定因素越大,它的耦合度也就越大。
  某類的不穩定因素=依賴的類個數/被依賴的類個數
  依賴的類個數= 在編譯此類的時被編譯的其它類的個數總和
  3.3.3怎樣將大系統拆分成小系統
  解決這個問題的一個思路是將許多類集合成一個更高層次的單位,形成一個高內聚、低耦合的類的集合,這是我們設計過程中應該着重考慮的問題!
  耦合的目標是維護依賴的單向性,有時我們也會需要使用壞的耦合。在這種情況下,應當小心記錄下原因,以幫助日後該代碼的用戶瞭解使用耦合真正的原因。
  3.4怎樣做到依賴倒轉?
  以抽象方式耦合是依賴倒轉原則的關鍵。抽象耦合關係總要涉及具體類從抽象類繼承,並且需要保證在任何引用到基類的地方都可以改換成其子類,因此,里氏代換原則是依賴倒轉原則的基礎。
  在抽象層次上的耦合雖然有靈活性,但也帶來了額外的複雜性,如果一個具體類發生變化的可能性非常小,那麼抽象耦合能發揮的好處便十分有限,這時可以用具體耦合反而會更好。
  層次化:所有結構良好的面向對象構架都具有清晰的層次定義,每個層次通過一個定義良好的、受控的接口向外提供一組內聚的服務。
  依賴於抽象:建議不依賴於具體類,即程序中所有的依賴關係都應該終止於抽象類或者接口。儘量做到:
  1、任何變量都不應該持有一個指向具體類的指針或者引用。
  2、任何類都不應該從具體類派生。
  3、任何方法都不應該覆寫它的任何基類中的已經實現的方法。
  3.5依賴倒轉原則的優缺點
  依賴倒轉原則雖然很強大,但卻最不容易實現。因爲依賴倒轉的緣故,對象的創建很可能要使用對象工廠,以避免對具體類的直接引用,此原則的使用可能還會導致產生大量的類,對不熟悉面向對象技術的工程師來說,維護這樣的系統需要較好地理解面向對象設計。
  依賴倒轉原則假定所有的具體類都是會變化的,這也不總是正確。有一些具體類可能是相當穩定,不會變化的,使用這個具體類實例的應用完全可以依賴於這個具體類型,而不必爲此創建一個抽象類型。
  四、合成/聚合複用原則(Composite/Aggregate Reuse Principle或CARP)
   4.1概念
  定義:在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分;新的對象通過向這些對象的委派達到複用這些對象的目的。
  應首先使用合成/聚合,合成/聚合則使系統靈活,其次才考慮繼承,達到複用的目的。而使用繼承時,要嚴格遵循里氏代換原則。有效地使用繼承會有助於對問題的理解,降低複雜度,而濫用繼承會增加系統構建、維護時的難度及系統的複雜度。
  如果兩個類是“Has-a”關係應使用合成、聚合,如果是“Is-a”關係可使用繼承。\"Is-A\"是嚴格的分類學意義上定義,意思是一個類是另一個類的\"一種\"。而\"Has-A\"則不同,它表示某一個角色具有某一項責任。

 4.2什麼是合成?什麼是聚合?
  合成(Composition)和聚合(Aggregation)都是關聯(Association)的特殊種類。
  聚合表示整體和部分的關係,表示“擁有”。如奔馳S360汽車,對奔馳S360引擎、奔馳S360輪胎的關係是聚合關係,離開了奔馳S360汽車,引擎、輪胎就失去了存在的意義。在設計中, 聚合不應該頻繁出現,這樣會增大設計的耦合度。[Page]
  合成則是一種更強的“擁有”,部分和整體的生命週期一樣。合成的新的對象完全支配其組成部分,包括它們的創建和湮滅等。一個合成關係的成分對象是不能與另一個合成關係共享的。
   換句話說,合成是值的聚合(Aggregation by Value),而一般說的聚合是引用的聚合(Aggregation by Reference)。
  明白了合成和聚合關係,再來理解合成/聚合原則應該就清楚了,要避免在系統設計中出現,一個類的繼承層次超過3層,則需考慮重構代碼,或者重新設計結構。當然最好的辦法就是考慮使用合成/聚合原則。
  4.3通過合成/聚合的優缺點
  優點:
  1) 新對象存取成分對象的唯一方法是通過成分對象的接口。
  2) 這種複用是黑箱複用,因爲成分對象的內部細節是新對象所看不見的。
  3) 這種複用支持包裝。
  4) 這種複用所需的依賴較少。
  5) 每一個新的類可以將焦點集中在一個任務上。
  6) 這種複用可以在運行時間內動態進行,新對象可以動態的引用與成分對象類型相同的對象。
  7) 作爲複用手段可以應用到幾乎任何環境中去。
  缺點:就是系統中會有較多的對象需要管理。
  4.4通過繼承來進行復用的優缺點
  優點:
  新的實現較爲容易,因爲超類的大部分功能可以通過繼承的關係自動進入子類。
  修改和擴展繼承而來的實現較爲容易。
   缺點:
  繼承複用破壞包裝,因爲繼承將超類的實現細節暴露給子類。由於超類的內部細節常常是對於子類透明的,所以這種複用是透明的複用,又稱“白箱”複用。
  如果超類發生改變,那麼子類的實現也不得不發生改變。
  從超類繼承而來的實現是靜態的,不可能在運行時間內發生改變,沒有足夠的靈活性。
  繼承只能在有限的環境中使用。
  五、 迪米特法則(Law of Demeter,LoD)
   5.1概述
   定義:一個軟件實體應當儘可能少的與其他實體發生相互作用。
   這樣,當一個模塊修改時,就會盡量少的影響其他的模塊。擴展會相對容易。
   這是對軟件實體之間通信的限制。它要求限制軟件實體之間通信的寬度和深度。
  5.2迪米特法則的其他表述:
  1)只與你直接的朋友們通信。
  2)不要跟“陌生人”說話。
  3)每一個軟件單位對其他的單位都只有最少的知識,而且侷限於那些與本單位密切相關的軟件單位。
  5.3狹義的迪米特法則
  如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中的一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。
  朋友圈的確定
  “朋友”條件:
  1)當前對象本身(this)
  2)以參量形式傳入到當前對象方法中的對象
  3)當前對象的實例變量直接引用的對象
  4)當前對象的實例變量如果是一個聚集,那麼聚集中的元素也都是朋友
  5)當前對象所創建的對象
  任何一個對象,如果滿足上面的條件之一,就是當前對象的“朋友”;否則就是“陌生人”。
  缺點:會在系統裏造出大量的小方法,散落在系統的各個角落。
  與依賴倒轉原則互補使用
  5.4狹義的迪米特法則的缺點:
  在系統裏造出大量的小方法,這些方法僅僅是傳遞間接的調用,與系統的商務邏輯無關。[Page]
  遵循類之間的迪米特法則會是一個系統的局部設計簡化,因爲每一個局部都不會和遠距離的對象有直接的關聯。但是,這也會造成系統的不同模塊之間的通信效率降低,也會使系統的不同模塊之間不容易協調。
  5.5迪米特法則與設計模式
  門面(外觀)模式和調停者(中介者)模式實際上就是迪米特法則的具體應用。
  5.6廣義的迪米特法則
  迪米特法則的主要用意是控制信息的過載。在將迪米特法則運用到系統設計中時,要注意下面的幾點:
  1)在類的劃分上,應當創建有弱耦合的類。
  2)在類的結構設計上,每一個類都應當儘量降低成員的訪問權限。
  3)在類的設計上,只要有可能,一個類應當設計成不變類。
  4)在對其他類的引用上,一個對象對其對象的引用應當降到最低。
  5.7廣義迪米特法則在類的設計上的體現
  1)優先考慮將一個類設置成不變類
  2)儘量降低一個類的訪問權限
  3)謹慎使用Serializable
  4)儘量降低成員的訪問權限
  5)取代C Struct
  迪米特法則又叫作最少知識原則(Least Knowledge Principle或簡寫爲LKP),就是說一個對象應當對其他對象有儘可能少的瞭解。
  5.8如何實現迪米特法則
  迪米特法則的主要用意是控制信息的過載,在將其運用到系統設計中應注意以下幾點:
  1) 在類的劃分上,應當創建有弱耦合的類。類之間的耦合越弱,就越有利於複用。
  2) 在類的結構設計上,每一個類都應當儘量降低成員的訪問權限。一個類不應當public自己的屬性,而應當提供取值和賦值的方法讓外界間接訪問自己的屬性。
  3) 在類的設計上,只要有可能,一個類應當設計成不變類。
  4) 在對其它對象的引用上,一個類對其它對象的引用應該降到最低。
  六、 接口隔離原則(interface separate principle, ISP)
   6.1概念
   接口隔離原則:使用多個專門的接口比使用單一的總接口要好。也就是說,一個類對另外一個類的依賴性應當是建立在最小的接口上。 
   這裏的\"接口\"往往有兩種不同的含義:一種是指一個類型所具有的方法特徵的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語言具體的\"接口\" 定義,有嚴格的定義和結構。比如c# 語言裏面的Interface結構。對於這兩種不同的含義,ISP的表達方式以及含義都有所不同。(上面說的一個類型,可以理解成一個類,我們定義了一個 類,也就是定義了一種新的類型) 
  當我們把\"接口\"理解成一個類所提供的所有方法的特徵集合的時候,這就是一種邏輯上的概念。接口的劃 分就直接帶來類型的劃分。這裏,我們可以把接口理解成角色,一個接口就只是代表一個角色,每個角色都有它特定的一個接口,這裏的這個原則可以叫做\"角色 隔離原則\"。 如果把\"接口\"理解成狹義的特定語言的接口,那麼ISP表達的意思是說,對不同的客戶端,同一個角色提供寬窄不同的接口,也就是定製服務,個性化服務。就是僅僅提供客戶端需要的行爲,客戶端不需要的行爲則隱藏起來。
   應當爲客戶端提供儘可能小的單獨的接口,而不要提供大的總接口。
   這也是對軟件實體之間通信的限制。但它限制的只是通信的寬度,就是說通信要儘可能的窄。[Page]
  遵循迪米特法則和接口隔離原則,會使一個軟件系統功能擴展時,修改的壓力不會傳到別的對象那裏。 
  6.2如何實現接口隔離原則
  不應該強迫用戶依賴於他們不用的方法。
  1、利用委託分離接口。
  2、利用多繼承分離接口。

發佈了51 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章