您的位置:首頁>設計>正文

物件導向設計的設計原則

在設計物件導向的程式的時, 模式不是一定要套的, 但是有一些原則最好是遵守。 這些原則已知的有七個, 包括:開閉原則、裡氏代換原則、依賴倒轉原則、合成/聚合複用原則、迪米特法則、介面隔離原則, 單一職責原則。

原則簡介

其中Robert C. Martin引入了SOLID的說法, 包括了其中的五個原則。 另外兩個, 這裡把他們算成額外的兩個規則。 具體如下。

S . Single Responsibility Principle - SRP

An object should have only a single responsibility.

O . Open-Closed Principle - OCP

Software entities should be opened for extension, but closed for modification.

L . Liskvo Substitution Principle - LSP

If S is a subtype of T, the objects of T in a program may be replaced by objects of Type S.

I . Interface Segregation Principle - ISP

many client specific interfaces are better than one general purpose interface.

D . Dependency Inversion Principle - DIP

Depend upon abstractions. Do not depend upon concretions.

Program to an interface, not an implementation.

Ex1 . Law of Demeter - LoD

-- Principle of Least Knowledge

A given object should assume as little as possible about the structure or properties of anything else.

Ex2 . Composite/Aggregate Reuse Principle - CARP

Use most composition and aggregation, less inheritance.

按照個人理解, 把這七個原則分成了以下兩個部分。

Design Method: SRP ISP DIP CARP

Design Goal: OCP LSP LoD

各規則詳細(本部分為轉載)

正如牛頓三大定律在經典力學中的位置一樣, “開-閉”原則(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 如何實現“開-閉” 原則

在物件導向設計中, 不允許更改的是系統的抽象層, 而允許擴展的是系統的實現層。 換言之, 定義一個一勞永逸的抽象設計層, 允許盡可能多的行為在實現層被實現。

解決問題關鍵在於抽象化, 抽象化是物件導向設計的第一個核心本質。

對一個事物抽象化, 實質上是在概括歸納總結它的本質。 抽象讓我們抓住最最重要的東西, 從更高一層去思考。 這降低了思考的複雜度, 我們不用同時考慮那麼多的東西。 換言之, 我們封裝了事物的本質, 看不到任何細節。

在物件導向程式設計中, 通過抽象類別及介面, 規定了具體類的特徵作為抽象層, 相對穩定, 不需更改, 從而滿足“對修改關閉”;而從抽象類別匯出的具體類可以改變系統的行為,

從而滿足“對擴展開放”。

對實體進行擴展時, 不必改動軟體的原始程式碼或者二進位碼。 關鍵在於抽象。

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

也就是說應當使用介面和抽象類別進行變數型別宣告、參數型別宣告、方法返還類型說明, 以及資料類型的轉換等。而不要用具體類進行變數的型別宣告、參數型別宣告、方法返還類型說明,以及資料類型的轉換等。要保證做到這一點,一個具體類應當只實現介面和抽象類別中聲明過的方法,而不要給出多餘的方法。

傳統的過程性系統的設計辦法傾向於使高層次的模組依賴於低層次的模組,抽象層次依賴於具體層次。倒轉原則就是把這個錯誤的依賴關係倒轉過來。

物件導向設計的重要原則是創建抽象化,並且從抽象化匯出具體化,具體化給出不同的實現。繼承關係就是一種從抽象化到具體化的匯出。

抽象層包含的應該是應用系統的商務邏輯和宏觀的、對整個系統來說重要的戰略性決定,是必然性的體現。具體層次含有的是一些次要的與實現有關的演算法和邏輯,以及戰術性的決定,帶有相當大的偶然性選擇。具體層次的代碼是經常變動的,不能避免出現錯誤。

從複用的角度來說,高層次的模組是應當複用的,而且是複用的重點,因為它含有一個應用系統最重要的宏觀商務邏輯,是較為穩定的。而在傳統的過程性設計中,複用則側重於具體層次模組的複用。

依賴倒轉原則則是對傳統的過程性設計方法的“倒轉”,是高層次模組複用及其可維護性的有效規範。

特例:物件的創建過程是違背“開—閉”原則以及依賴倒轉原則的,但通過工廠模式,能很好地解決物件創建過程中的依賴倒轉問題。

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大部分都是不當耦合造成的 。

如果不穩定因素越大,它的耦合度也就越大。

某類的不穩定因素=依賴的類個數/被依賴的類個數

依賴的類個數= 在編譯此類的時被編譯的其它類的個數總和

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汽車,引擎、輪胎就失去了存在的意義。在設計中, 聚合不應該頻繁出現,這樣會增大設計的耦合度。

合成則是一種更強的“擁有”,部分和整體的生命週期一樣。合成的新的物件完全支 配其組成部分,包括它們的創建和湮滅等。一個合成關係的成分物件是不能與另一個合成關係共用的。

換句話說,合成是值的聚合(Aggregation by Value),而一般說的聚合是引用的聚合(Aggregation by Reference)。

明白了合成和聚合關係,再來理解合成/聚合原則應該就清楚了,要避免在系統設計中出現,一個類的繼承層次超過3層,則需考慮重構代碼,或者重新設計結構。當然最好的辦法就是考慮使用合成/聚合原則。

4.3 通過合成/聚合的優缺點

優點:

1) 新物件存取成分物件的唯一方法是通過成分物件的介面。

2) 這種複用是黑箱複用,因為成分物件的內部細節是新物件所看不見的。

3) 這種複用支持包裝。

4) 這種複用所需的依賴較少。

5) 每一個新的類可以將焦點集中在一個任務上。

6) 這種複用可以在執行時間內動態進行,新物件可以動態的引用與成分物件類型相同的物件。

7) 作為複用手段可以應用到幾乎任何環境中去。

缺點:就是系統中會有較多的物件需要管理。

4.4 通過繼承來進行複用的優缺點

優點:

1) 新的實現較為容易,因為超類的大部分功能可以通過繼承的關係自動進入子類。

2) 修改和擴展繼承而來的實現較為容易。

缺點 :

1) 繼承複用破壞封裝,因為繼承將超類的實現細節暴露給子類。由於超類的內部細節常常是對於子類透明的,所以這種複用是透明的複用,又稱“白箱”複用。

2) 如果超類發生改變,那麼子類的實現也不得不發生改變。

3) 從超類繼承而來的實現是靜態的,不可能在執行時間內發生改變,沒有足夠的靈活性。

4) 繼承只能在有限的環境中使用。

五、 迪米特法則( Law of Demeter ,LoD )

5.1 概述

定義:一個軟體實體應當盡可能少的與其他實體發生相互作用。

這樣,當一個模組修改時,就會儘量少的影響其他的模組。擴展會相對容易。

這是對軟體實體之間通信的限制。它要求限制軟體實體之間通信的寬度和深度。

5.2 迪米特法則的其他表述

1)只與你直接的朋友們通信。

2)不要跟“陌生人”說話。

3)每一個軟體單位對其他的單位都只有最少的知識,而且局限於那些與本單位密切相關的軟體單位。

5.3 狹義的迪米特法則

如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中的一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。

朋友圈的確定,“朋友”條件:

1)當前物件本身(this)

2)以參量形式傳入到當前物件方法中的物件

3)當前物件的執行個體變數直接引用的物件

4)當前物件的執行個體變數如果是一個聚集,那麼聚集中的元素也都是朋友

5)當前物件所創建的物件

任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”;否則就是“陌生 人”。

缺點:會在系統裡造出大量的小方法,散落在系統的各個角落。

與依賴倒轉原則互補使用。

5.4 狹義的迪米特法則的缺點:

在系統裡造出大量的小方法,這些方法僅僅是傳遞間接的調用,與系統的商務邏輯無關。

遵循類之間的迪米特法則會是一個系統的局部設計簡化,因為每一個局部都不會和遠距離的物件有直接的關聯。但是,這也會造成系統的不同模組之間的通信效率降低,也會使系統的不同模組之間不容易協調。

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 Segregation Principle, ISP )

6.1 概念

介面隔離原則:使用多個專門的介面比使用單一的總介面要好。也就是說,一個類對另外一個類的依賴性應當是建立在最小的介面上。

這裡的"介面"往往有兩種不同的含義:一種是指一個類型所具有的方法特徵的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語言具體的"介面"定義,有嚴格的定義和結構。比如C#語言裡面的Interface結構。對於這兩種不同的含義,ISP的表達方式以及含義都有所不同。(上面說的一個類型,可以理解成一個類,我們定義了一個 類,也就是定義了一種新的類型)

當我們把"介面"理解成一個類所提供的所有方法的特徵集合的時候,這就是一種邏輯上的 概念。介面的劃分就直接帶來類型的劃分。這裡,我們可以把介面理解成角色,一個介面就只是代表一個角色,每個角色都有它特定的一個介面,這裡的這個原則可以叫做"角色隔離原則"。

如果把"介面"理解成狹義的特定語言的介面,那麼ISP表達的意思是說,對不同的用戶端,同一個角色提供寬窄不同的介面,也就是定制服務,個性化服務。就是僅僅提供用戶端需要的行為,用戶端不需要的行為則隱藏起來。

應當為用戶端提供盡可能小的單獨的介面,而不要提供大的總介面。

這也是對軟體實體之間通信的限制。但它限制的只是通信的寬度,就是說通信要盡可能的窄。

遵循迪米特法則和介面隔離原則,會使一個軟體系統功能擴展時,修改的壓力不會傳 到別的物件那裡。

6.2 如何實現介面隔離原則

不應該強迫用戶依賴於他們不用的方法。

1、利用委託分離介面。

2、利用多繼承分離介面。

七、單一職責原則(SRP)

單一職責原則 (SRP),就一個類而言,應該僅有一個引起它變化的原因。也就是說,不要把變化原因各不相同的職責放在一起,因為不同的變化會影響到不相干的職責。再通俗一點地說就是,不該你管的事情你不要管,管好自己的事情就可以了,多管閒事害了自己也害了別人。

在軟體設計中,如果一個類承擔的職責過多,就等於吧這些職責耦合在一起,而一個職責的變化可能會削弱和抑制這個類完成其他職責的能力。這耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞。

軟體設計真正要做的許多內容,就是發現職責並把那些職責相互分離。如果多於一個的動機去改變一個類,那麼這個類就具有多餘一個的職責,就應該要考慮類的職責分離。

小結

在我們進行物件導向系統的設計時,可以不去特意的考慮使用哪些設計模式,但是一定要儘量遵守這些設計原則。這樣做的話,即使是設計經驗不足,也比較容易設 計出易擴展的系統,並且可能自然的實現了某些模式。這種情況,恐怕算是很理想的一種設計了。

以及資料類型的轉換等。而不要用具體類進行變數的型別宣告、參數型別宣告、方法返還類型說明,以及資料類型的轉換等。要保證做到這一點,一個具體類應當只實現介面和抽象類別中聲明過的方法,而不要給出多餘的方法。

傳統的過程性系統的設計辦法傾向於使高層次的模組依賴於低層次的模組,抽象層次依賴於具體層次。倒轉原則就是把這個錯誤的依賴關係倒轉過來。

物件導向設計的重要原則是創建抽象化,並且從抽象化匯出具體化,具體化給出不同的實現。繼承關係就是一種從抽象化到具體化的匯出。

抽象層包含的應該是應用系統的商務邏輯和宏觀的、對整個系統來說重要的戰略性決定,是必然性的體現。具體層次含有的是一些次要的與實現有關的演算法和邏輯,以及戰術性的決定,帶有相當大的偶然性選擇。具體層次的代碼是經常變動的,不能避免出現錯誤。

從複用的角度來說,高層次的模組是應當複用的,而且是複用的重點,因為它含有一個應用系統最重要的宏觀商務邏輯,是較為穩定的。而在傳統的過程性設計中,複用則側重於具體層次模組的複用。

依賴倒轉原則則是對傳統的過程性設計方法的“倒轉”,是高層次模組複用及其可維護性的有效規範。

特例:物件的創建過程是違背“開—閉”原則以及依賴倒轉原則的,但通過工廠模式,能很好地解決物件創建過程中的依賴倒轉問題。

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大部分都是不當耦合造成的 。

如果不穩定因素越大,它的耦合度也就越大。

某類的不穩定因素=依賴的類個數/被依賴的類個數

依賴的類個數= 在編譯此類的時被編譯的其它類的個數總和

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汽車,引擎、輪胎就失去了存在的意義。在設計中, 聚合不應該頻繁出現,這樣會增大設計的耦合度。

合成則是一種更強的“擁有”,部分和整體的生命週期一樣。合成的新的物件完全支 配其組成部分,包括它們的創建和湮滅等。一個合成關係的成分物件是不能與另一個合成關係共用的。

換句話說,合成是值的聚合(Aggregation by Value),而一般說的聚合是引用的聚合(Aggregation by Reference)。

明白了合成和聚合關係,再來理解合成/聚合原則應該就清楚了,要避免在系統設計中出現,一個類的繼承層次超過3層,則需考慮重構代碼,或者重新設計結構。當然最好的辦法就是考慮使用合成/聚合原則。

4.3 通過合成/聚合的優缺點

優點:

1) 新物件存取成分物件的唯一方法是通過成分物件的介面。

2) 這種複用是黑箱複用,因為成分物件的內部細節是新物件所看不見的。

3) 這種複用支持包裝。

4) 這種複用所需的依賴較少。

5) 每一個新的類可以將焦點集中在一個任務上。

6) 這種複用可以在執行時間內動態進行,新物件可以動態的引用與成分物件類型相同的物件。

7) 作為複用手段可以應用到幾乎任何環境中去。

缺點:就是系統中會有較多的物件需要管理。

4.4 通過繼承來進行複用的優缺點

優點:

1) 新的實現較為容易,因為超類的大部分功能可以通過繼承的關係自動進入子類。

2) 修改和擴展繼承而來的實現較為容易。

缺點 :

1) 繼承複用破壞封裝,因為繼承將超類的實現細節暴露給子類。由於超類的內部細節常常是對於子類透明的,所以這種複用是透明的複用,又稱“白箱”複用。

2) 如果超類發生改變,那麼子類的實現也不得不發生改變。

3) 從超類繼承而來的實現是靜態的,不可能在執行時間內發生改變,沒有足夠的靈活性。

4) 繼承只能在有限的環境中使用。

五、 迪米特法則( Law of Demeter ,LoD )

5.1 概述

定義:一個軟體實體應當盡可能少的與其他實體發生相互作用。

這樣,當一個模組修改時,就會儘量少的影響其他的模組。擴展會相對容易。

這是對軟體實體之間通信的限制。它要求限制軟體實體之間通信的寬度和深度。

5.2 迪米特法則的其他表述

1)只與你直接的朋友們通信。

2)不要跟“陌生人”說話。

3)每一個軟體單位對其他的單位都只有最少的知識,而且局限於那些與本單位密切相關的軟體單位。

5.3 狹義的迪米特法則

如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中的一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。

朋友圈的確定,“朋友”條件:

1)當前物件本身(this)

2)以參量形式傳入到當前物件方法中的物件

3)當前物件的執行個體變數直接引用的物件

4)當前物件的執行個體變數如果是一個聚集,那麼聚集中的元素也都是朋友

5)當前物件所創建的物件

任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”;否則就是“陌生 人”。

缺點:會在系統裡造出大量的小方法,散落在系統的各個角落。

與依賴倒轉原則互補使用。

5.4 狹義的迪米特法則的缺點:

在系統裡造出大量的小方法,這些方法僅僅是傳遞間接的調用,與系統的商務邏輯無關。

遵循類之間的迪米特法則會是一個系統的局部設計簡化,因為每一個局部都不會和遠距離的物件有直接的關聯。但是,這也會造成系統的不同模組之間的通信效率降低,也會使系統的不同模組之間不容易協調。

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 Segregation Principle, ISP )

6.1 概念

介面隔離原則:使用多個專門的介面比使用單一的總介面要好。也就是說,一個類對另外一個類的依賴性應當是建立在最小的介面上。

這裡的"介面"往往有兩種不同的含義:一種是指一個類型所具有的方法特徵的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語言具體的"介面"定義,有嚴格的定義和結構。比如C#語言裡面的Interface結構。對於這兩種不同的含義,ISP的表達方式以及含義都有所不同。(上面說的一個類型,可以理解成一個類,我們定義了一個 類,也就是定義了一種新的類型)

當我們把"介面"理解成一個類所提供的所有方法的特徵集合的時候,這就是一種邏輯上的 概念。介面的劃分就直接帶來類型的劃分。這裡,我們可以把介面理解成角色,一個介面就只是代表一個角色,每個角色都有它特定的一個介面,這裡的這個原則可以叫做"角色隔離原則"。

如果把"介面"理解成狹義的特定語言的介面,那麼ISP表達的意思是說,對不同的用戶端,同一個角色提供寬窄不同的介面,也就是定制服務,個性化服務。就是僅僅提供用戶端需要的行為,用戶端不需要的行為則隱藏起來。

應當為用戶端提供盡可能小的單獨的介面,而不要提供大的總介面。

這也是對軟體實體之間通信的限制。但它限制的只是通信的寬度,就是說通信要盡可能的窄。

遵循迪米特法則和介面隔離原則,會使一個軟體系統功能擴展時,修改的壓力不會傳 到別的物件那裡。

6.2 如何實現介面隔離原則

不應該強迫用戶依賴於他們不用的方法。

1、利用委託分離介面。

2、利用多繼承分離介面。

七、單一職責原則(SRP)

單一職責原則 (SRP),就一個類而言,應該僅有一個引起它變化的原因。也就是說,不要把變化原因各不相同的職責放在一起,因為不同的變化會影響到不相干的職責。再通俗一點地說就是,不該你管的事情你不要管,管好自己的事情就可以了,多管閒事害了自己也害了別人。

在軟體設計中,如果一個類承擔的職責過多,就等於吧這些職責耦合在一起,而一個職責的變化可能會削弱和抑制這個類完成其他職責的能力。這耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞。

軟體設計真正要做的許多內容,就是發現職責並把那些職責相互分離。如果多於一個的動機去改變一個類,那麼這個類就具有多餘一個的職責,就應該要考慮類的職責分離。

小結

在我們進行物件導向系統的設計時,可以不去特意的考慮使用哪些設計模式,但是一定要儘量遵守這些設計原則。這樣做的話,即使是設計經驗不足,也比較容易設 計出易擴展的系統,並且可能自然的實現了某些模式。這種情況,恐怕算是很理想的一種設計了。

Next Article
喜欢就按个赞吧!!!
点击关闭提示