1. 程式人生 > >Java面向物件16種設計原則(總結版)

Java面向物件16種設計原則(總結版)

Java面向物件16種設計原則

一   類的設計原則

1 依賴倒置原則-Dependency Inversion Principle (DIP)

2 里氏替換原則-Liskov Substitution Principle (LSP)

3 介面分隔原則-Interface Segregation Principle (ISP)

4 單一職責原則-Single Responsibility Principle (SRP)

5 開閉原則-The Open-Closed Principle (OCP)

二  包的設計原則

6 重用釋出等價原則-Release ReuseEquivalency Principle (REP)

7 無環依賴原則-The AcyclicDependencies Principle (ADP)

8 穩定依賴原則-The StableDependencies Principle (SDP)

9 穩定抽象等價原則-The StableAbstractions Principle (SAP)

10 共同封閉原則-The CommonClosure Principle (CCP)

11 全部重用原則-The Common Reuse Principle (CRP)

三  擴充套件原則


12  迪米特法則 -Least Knowledge Principle (LKP)
13  黑盒原則- BBP(Black Box Principle)
14  預設抽象原則 -DAP(Default Abstraction Principle)
15  介面設計原則 -IDP(Interface Design Principle)
16  不要構造具體的超類原則 -DCSP(Don't Concrete SupperclassPrinciple)

1. Dependency Inversion Principle (DIP) - 依賴倒置原則

依賴:在程式設計中,如果一個模組a使用或呼叫了另一個模組b,我們稱模組a依賴模組b。
高層模組與低層模組:往往在一個應用程式中,我們有一些低層次的類,這些類實現了一些基本的或初級的操作,我們稱之為低層模組;另外有一些高層次的類,這些類封裝了某些複雜的邏輯,並且依賴於低層次的類,這些類我們稱之為高層模組。

依賴倒置原則的2個重要方針


A. 高層模組不應該依賴於低層模組,二者都應該依賴於抽象
B. 抽象不應該依賴於細節,細節應該依賴於抽象

為什麼叫做依賴倒置(Dependency Inversion)呢?


面向物件程式設計相對於面向過程(結構化)程式設計而言,依賴關係被倒置了。因為傳統的結構化程式設計中,高層模組總是依賴於低層模組。

問題的提出:

Robert C. Martin在原文中給出了“Bad Design”的定義:
1. 系統很難改變,因為每個改變都會影響其他很多部分。
2. 當你對某地方做一修改,系統的看似無關的其他部分都不工作了。
3. 系統很難被另外一個應用重用,因為你很難將要重用的部分從系統中分離開來。

導致“Bad Design”的很大原因是“高層模組”過分依賴“低層模組”。一個良好的設計應該是系統的每一部分都是可替換的。如果“高層模組”過分依賴“低層模組”,一方面一旦“低層模組”需要替換或者修改,“高層模組”將受到影響;另一方面,高層模組很難可以重用。

比如,一個Copy模組,需要把來自Keyboard的輸入複製到Print,即使對Keyboard和Print的封裝已經做得非常好,但如果Copy模組裡直接使用Keyboard與Print,Copy任很難被其他應用環境(比如需要輸出到磁碟時)重用。

問題的解決:

為了解決上述問題,Robert C. Martin提出了OO設計的Dependency Inversion Principle (DIP) 原則。
DIP給出了一個解決方案:在高層模組與低層模組之間,引入一個抽象介面層。
High Level Classes(高層模組) --> Abstraction Layer(抽象介面層) --> Low Level Classes(低層模組)
抽象介面是對低層模組的抽象,低層模組繼承或實現該抽象介面。
這樣,高層模組不直接依賴低層模組,高層模組與低層模組都依賴抽象介面層。
當然,抽象也不依賴低層模組的實現細節,低層模組依賴(繼承或實現)抽象定義。

Robert C. Martin給出的DIP方案的類的結構圖:


PolicyLayer-->MechanismInterface(abstract)--MechanismLayer-->UtilityInterface(abstract)--UtilityLayer
類與類之間都通過Abstract Layer來組合關係。

2. Liskov Substitution Principle (LSP) - 里氏替換原則

所有引用基類的地方必須能透明地使用其子類的物件。也就是說,只有滿足以下2個條件的OO設計才可被認為是滿足了LSP原則:

不應該在程式碼中出現if/else之類對子類型別進行判斷的條件。以下程式碼就違反LSP定義。
if (obj typeof Class1) {
     do something
} else if (obj typeof Class2) {
     do something else
}
子類應當可以替換父類並出現在父類能夠出現的任何地方,或者說如果我們把程式碼中使用基類的地方用它的子類所代替,程式碼還能正常工作。

里氏替換原則LSP是使程式碼符合開閉原則的一個重要保證。同時LSP體現了:
1) 類的繼承原則:如果一個繼承類的物件可能會在基類出現地方出現執行錯誤,則該子類不應該從該基類繼承,或者說,應該重新設計它們之間的關係。
2)動作正確性保證:從另一個側面上保證了符合LSP設計原則的類的擴充套件不會給已有的系統引入新的錯誤。

類的繼承原則:
Robert C. Martin舉了Rectangle和Square的例子。這裡沿用這個例子,但用Java語言對其加以重寫,並忽略了某些細節只列出下面的精要部分來說明 里氏替換原則 對類的繼承上的約束。

  1. class Rectangle {   
  2.     double width;   
  3.     double height;   
  4.     public double getHeight() {   
  5.         return height;   
  6.      }   
  7.     public void setHeight(double height) {   
  8.         this.height = height;   
  9.      }   
  10.     public double getWidth() {   
  11.         return width;   
  12.      }   
  13.     public void setWidth(double width) {   
  14.         this.width = width;   
  15.      }      
  16. }   
  17. class Square extends Rectangle {   
  18.     public void setHeight(double height) {   
  19.         super.setHeight(height);   
  20.         super.setWidth(height);   
  21.      }   
  22.     public void setWidth(double width) {   
  23.         super.setHeight(width);   
  24.         super.setWidth(width);   
  25.      }   
  26. }  

class Rectangle {

      double width;

      double height;

      public double getHeight() {

          return height;

      }

      public void setHeight(double height) {

          this.height = height;

      }

      public double getWidth() {

          return width;

      }

      public void setWidth(double width) {

          this.width = width;

      }    

}

class Square extends Rectangle {

      public void setHeight(double height) {

          super.setHeight(height);

          super.setWidth(height);

      }

      public void setWidth(double width) {

          super.setHeight(width);

          super.setWidth(width);

      }

}



這裡Rectangle是基類,Square從Rectangle繼承。這種繼承關係有什麼問題嗎?

假如已有的系統中存在以下既有的業務邏輯程式碼:
void g(Rectangle r) {
     r.setWidth(5);
     r.setHeight(4);
     if (r.getWidth() * r.getHeight() != 20) {
         throw new RuntimeException();
     }
}

則對應於擴充套件類Square,在呼叫既有業務邏輯時:
         Rectangle square = new Square();
         g(square);
時會丟擲一個RuntimeException異常。這顯然違反了LSP原則。

動作正確性保證:
因為LSP對子類的約束,所以為已存在的類做擴充套件構造一個新的子類時,根據LSP的定義,不會給已有的系統引入新的錯誤。

Design by Contract

         根據Bertrand Meyer提出的Design by Contract(DBC:基於合同的設計)概念的描述,對於類的一個方法,都有一個前提條件以及一個後續條件,前提條件說明方法接受什麼樣的引數資料等,只有前提條件得到滿足時,這個方法才能被呼叫;同時後續條件用來說明這個方法完成時的狀態,如果一個方法的執行會導致這個方法的後續條件不成立,那麼此方法也不應該正常返回。
         現在把前提條件以及後續條件應用到繼承子類中,子類方法應該滿足:
1)前提條件不強於基類.
2)後續條件不弱於基類.
        換句話說,通過基類的介面呼叫一個物件時,使用者只知道基類前提條件以及後續條件。因此繼承類不得要求使用者提供比基類方法要求的更強的前提條件,亦即,繼承類方法必須接受任何基類方法能接受的任何條件(引數)。同樣,繼承類必須順從基類的所有後續條件,亦即,繼承類方法的行為和輸出不得違反由基類建立起來的任何約束,不能讓使用者對繼承類方法的輸出感到困惑。這樣,我們就有了基於合同的LSP,基於合同的LSP是LSP的一種強化。

在很多情況下,在設計初期我們類之間的關係不是很明確,LSP則給了我們一個判斷和設計類之間關係的基準:需不需要繼承,以及怎樣設計繼承關係。

3. Interface Segregation Principle (ISP) - 介面分隔原則


不能強迫使用者去依賴那些他們不使用的介面。換句話說,使用多個專門的介面比使用單一的總介面總要好。它包含了2層意思:
1)介面的設計原則:介面的設計應該遵循最小介面原則,不要把使用者不使用的方法塞進同一個接口裡。
   如果一個介面的方法沒有被使用到,則說明該介面過胖,應該將其分割成幾個功能專一的介面。
2)介面的依賴(繼承)原則:如果一個介面a依賴(繼承)另一個介面b,則介面a相當於繼承了介面b的方法,那麼繼承了介面b後的介面a也應該遵循上述原則:不應該包含使用者不使用的方法。   反之,則說明介面a被b給汙染了,應該重新設計它們的關係。

如果使用者被迫依賴他們不使用的介面,當介面發生改變時,他們也不得不跟著改變。換而言之,一個使用者依賴了未使用但被其他使用者使用的介面,當其他使用者修改該介面時,依賴該介面的所有使用者都將受到影響。這顯然違反了開閉原則,也不是我們所期望的。

下面我們舉例說明怎麼設計介面或類之間的關係,使其不違反ISP原則。
假如有一個Door,有lock,unlock功能,另外,可以在Door上安裝一個Alarm而使其具有報警功能。使用者可以選擇一般的Door,也可以選擇具有報警功能的Door。

有以下幾種設計方法:

ISP原則的違反例:


方法一:
在Door接口裡定義所有的方法。圖:

但這樣一來,依賴Door介面的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。

方法二:
在Alarm介面定義alarm方法,在Door介面定義lock,unlock方法,Door介面繼承Alarm介面。


跟方法一一樣,依賴Door介面的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。

遵循ISP原則的例:


方法三:通過多重繼承實現

Adapter設計模式的實現。
第2)種方案更具有實用性。
這種設計遵循了ISP設計原則。

方法四:通過委託實現

在Alarm介面定義alarm方法,在Door介面定義lock,unlock方法。介面之間無繼承關係。CommonDoor實現Door介面,
AlarmDoor有2種實現方案:
1)同時實現Door和Alarm介面。
2)繼承CommonDoor,並實現Alarm介面。該方案是繼承方式的

小結

Interface Segregation Principle (ISP)從對介面的使用上為我們對介面抽象的顆粒度建立了判斷基準:在為系統設計介面的時候,使用多個專門的介面代替單一的胖介面。

4. Single Responsibility Principle (SRP) - 單一職責原則

永遠不要讓一個類存在多個改變的理由。換句話說,如果一個類需要改變,改變它的理由永遠只有一個。如果存在多個改變它的理由,就需要重新設計該類。

SRP(Single Responsibility Principle)原則的核心含意是:只能讓一個類有且僅有一個職責。這也是單一職責原則的命名含義。

為什麼一個類不能有多於一個以上的職責呢?
如果一個類具有一個以上的職責,那麼就會有多個不同的原因引起該類變化,而這種變化將影響到該類不同職責的使用者(不同使用者):
1,一方面,如果一個職責使用了外部類庫,則使用另外一個職責的使用者卻也不得不包含這個未被使用的外部類庫。
2,另一方面,某個使用者由於某原因需要修改其中一個職責,另外一個職責的使用者也將受到影響,他將不得不重新編譯和配置。這違反了設計的開閉原則,也不是我們所期望的。

職責的劃分

既然一個類不能有多個職責,那麼怎麼劃分職責呢?
Robert.C Martin給出了一個著名的定義:所謂一個類的一個職責是指引起該類變化的一個原因。如果你能想到一個類存在多個使其改變的原因,那麼這個類就存在多個職責。

Single Responsibility Principle (SRP)的原文裡舉了一個Modem的例子來說明怎麼樣進行職責的劃分,這裡我們也沿用這個例子來說明一下:

SRP違反例:
Modem.java
interface Modem {
     public void dial(String pno);     //撥號
     public void hangup();         //結束通話
     public void send(char c);     //傳送資料
     public char recv();         //接收資料
}
咋一看,這是一個沒有任何問題的介面設計。但事實上,這個介面包含了2個職責:第一個是連線管理(dial, hangup);另一個是資料通訊(send, recv)。很多情況下,這2個職責沒有任何共通的部分,它們因為不同的理由而改變,被不同部分的程式呼叫。
所以它違反了SRP原則。

下面的類圖將它的2個不同職責分成2個不同的介面,這樣至少可以讓客戶端應用程式使用具有單一職責的介面:

讓ModemImplementation實現這兩個介面。我們注意到,ModemImplementation又組合了2個職責,這不是我們希望的,但有時這又是必須的。通常由於某些原因,迫使我們不得不繫結多個職責到一個類中,但我們至少可以通過介面的分割來分離應用程式關心的概念。
事實上,這個例子一個更好的設計應該是這樣的,如圖:

小結

Single Responsibility Principle (SRP)從職責(改變理由)的側面上為我們對類(介面)的抽象的顆粒度建立了判斷基準:在為系統設計類(介面)的時候應該保證它們的單一職責性。

5. The Open-Closed Principle (OCP) - 開閉原則

開閉原則(OCP:Open-Closed Principle)是指在進行面向物件設計(OOD:Object Oriented Design)中,設計類或其他程式單位時,應該遵循:
- 對擴充套件開放(open)
- 對修改關閉(closed)
 
開閉原則是判斷面向物件設計是否正確的最基本的原理之一。 根據開閉原則,在設計一個軟體系統模組(類,方法)的時候,應該可以在不修改原有的模組(修改關閉)的基礎上,能擴充套件其功能(擴充套件開放)。
A  擴充套件開放:某模組的功能是可擴充套件的,則該模組是擴充套件開放的。軟體系統的功能上的可擴充套件性要求模組是擴充套件開放的。
 B 修改關閉:某模組被其他模組呼叫,如果該模組的原始碼不允許修改,則該模組修改關閉的。軟體系統的功能上的穩定性,持續性要求是修改關閉的。

這也是系統設計需要遵循開閉原則的原因
1)穩定性。開閉原則要求擴充套件功能不修改原來的程式碼,這可以讓軟體系統在變化中保持穩定。
2)擴充套件性。開閉原則要求對擴充套件開放,通過擴充套件提供新的或改變原有的功能,讓軟體系統具有靈活的可擴充套件性。
遵循開閉原則的系統設計,可以讓軟體系統可複用,並且易於維護。

開閉原則的實現方法

為了滿足開閉原則的 對修改關閉(closed for modification) 原則以及擴充套件開放(open for extension) 原則,應該對軟體系統中的不變的部分加以抽象,在面向物件的設計中,
A 可以把這些不變的部分加以抽象成不變的介面,這些不變的介面可以應對未來的擴充套件;
B 介面的最小功能設計原則。根據這個原則,原有的介面要麼可以應對未來的擴充套件;不足的部分可以通過定義新的介面來實現;
C 模組之間的呼叫通過抽象介面進行,這樣即使實現層發生變化,也無需修改呼叫方的程式碼。

介面可以被複用,但介面的實現卻不一定能被複用。介面是穩定的,關閉的,但介面的實現是可變的,開放的。可以通過對介面的不同實現以及類的繼承行為等為系統增加新的或改變系統原來的功能,實現軟體系統的柔軟擴充套件。

         簡單地說,軟體系統是否有良好的介面(抽象)設計是判斷軟體系統是否滿足開閉原則的一種重要的判斷基準。現在多把開閉原則等同於面向介面的軟體設計。

開閉原則的相對性

軟體系統的構建是一個需要不斷重構的過程,在這個過程中,模組的功能抽象,模組與模組間的關係,都不會從一開始就非常清晰明瞭,所以構建100%滿足開閉原則的軟體系統是相當困難的,這就是開閉原則的相對性。但在設計過程中,通過對模組功能的抽象(介面定義),模組之間的關係的抽象(通過介面呼叫),抽象與實現的分離(面向介面的程式設計)等,可以儘量接近滿足開閉原則。

6. Release Reuse Equivalency Principle (REP) - 重用釋出等價原則

 
包的內部關係方面(聚合性)的原則,重用粒度等價於釋出粒度。重用主要是從使用者的觀點來看的。對使用者來說,使用某個釋出單位(元件,類,類群等),如果作者因為某種原因對其作了修改而釋出了一個新的版本,使用者會期望在升級為新版本之後,不會影響到原系統的正常運作。
也就是說,對於一個可重用(能供其它使用者或系統使用)的元素(元件,類,類群等),作者應該承諾新版本能夠相容舊版本。否則,使用者將拒絕使用該元素。

Robert C. Martin給出的對重用的定義:
程式碼可以看作為可重用的程式碼,當且僅當:
- 它的使用者(下稱使用者)無需看它的原始碼
- 使用者只需聯結靜態庫或包含動態庫
- 當庫發生改變(錯誤糾正,功能增強)時,使用者只需要得到一個新的版本便能整合到原有的系統

怎麼做到重用呢?
一個元件要做到能夠重用,它必須有一個得到良好設計的結構,它所包含所有元素必須也是可以重用的。
因為如果一個為重用而設計的釋出單位裡,包含了不可重用的元素,當不可重用的元素髮生改變時,使用者也不得不改變原有系統以適應新的版本。這顯然違反了重用的定義規則。
也就是說,一個為重用目的而設計的釋出單位裡,不能包含不可重用的元素;如果包含了不可重用的元素,它將變得不可重用。

釋出單位

當用戶使用的重用元件被作者修改後,使用者希望得到通知,然後決定是否升級或升級的時機。為了能使升級後的系統也能正常運作,使用者也希望作者有一個規範的釋出,包括版本號,類庫的說明等等。
一旦使用者決定升級新版本,不管這些修改是否影響到使用者,使用者也不得不包含新版本所包含的所有類庫。

REP規定重用粒度不能小於釋出粒度,所有重用元素也必須被一起釋出。

釋出粒度可以為包(元件)或類等實體,但一個應用往往包含了很多類,所以,具有更大的尺度的包(元件)更加適合作為釋出粒度。

重用釋出等價原則為我們指明瞭包的設計方針:一個包中的元素(類)要麼都可重用,要麼都不可重用。

小結

重用釋出等價原則(REP)從使用者觀點的角度上為我們規範了包設計的原則:在設計包時,包中應該包含的元素要麼都可以重用,要麼都不可以重用。

7. The Acyclic Dependencies Principle (ADP) - 無環依賴原則

包之間的依賴結構必須是一個直接的無環圖形(DAG)。也就是說,在依賴結構中不允許出現環(迴圈依賴)。換成另一個說法是: 包間依賴不能是一個環狀形式。包間關係方面(耦合性)的原則

包的依賴
如果一個包A 中的類引用了包B中的類,我們稱包A依賴包B。
“依賴”在具體的程式語言中表現為,如果A依賴B,C/C++語言則在A包的檔案/類中通過#include語句包含B包中的檔案/類;Java語言則A包的類中通過import語句引入B包中的類。


圖1(包A依賴包B)
虛線表示一種依賴關係,箭頭表示依賴的方向,箭頭所在的一側就是被依賴的包。

包的迴圈依賴
我們上面討論了並用圖形表示了包之間的依賴關係。如果存在2個或2個以上的包,它們之間的依賴關係圖出現了環狀,我們就稱包之間存在迴圈依賴關係。
也就是說它們的依賴結構圖根據箭頭的方向形成了一個環狀的閉合圖形。如圖:

圖2:包的迴圈依賴
如圖:A依賴B,B依賴C,C依賴A,形成了一個環狀依賴。

包的非迴圈依賴原則

包是一個比較合適的釋出粒度,當修改了包中的程式碼(類,模組等)併發布新的版本時,我們需要把該包以及它所依賴的其它包一起釋出。釋出之後,還需要驗證系統是否能在新發布的版本下正常運作。
如果多個包之間形成了迴圈依賴,比如如圖2,A依賴B,B依賴C,C依賴A,我們修改了B並需要釋出B的一個新的版本,因為B依賴C,所以釋出時應該包含C,但C同時又依賴A,所以又應該把A也包含進發布版本里。也就是說,依賴結構中,出現在環內的所有包都不得不一起釋出。它們形成了一個高耦合體,當專案的規模大到一定程度,包的數目變多時,包與包之間的關係便變得錯綜複雜,各種測試也將變得非常困難,常常會因為某個不相關的包中的錯誤而使得測試無法繼續。而釋出也變得複雜,需要把所有的包一起釋出,無疑增加了釋出後的驗證難度。

迴圈依賴的打破方法

如果包的依賴形成了環狀結構,怎麼樣打破這種迴圈依賴呢?
有2種方法可以打破這種迴圈依賴關係:第一種方法是建立新的包,第二種方法是使用DIP(依賴倒置原則)ISP(介面分隔原則)設計原則。

方法一:建立新的包
比如對於圖2這種依賴結構:


圖2:包的迴圈依賴

包C要依賴包A,必定A中包含有A,C共通使用的類,把這些共同類抽出來放在一個新的包D裡。這樣就把C依賴A變成了C依賴D以及A依賴D,從而打破了迴圈依賴關係。如圖:


這樣,包的依賴關係就從A->B->C->A變成了:
A->B->C->D
A->D

方法二:DIP與ISP設計原則
ISP(介面分隔原則)可以剔除美用到的介面。DIP(依賴倒置原則)在類的呼叫之間引入抽象層。

如圖,,包A依賴包B(因為包A中的類U使用了包B中的類X);反過來,包B又依賴包A(因為包B中的類Y使用了包A中的類V)


包A,包B之間就形成了一種迴圈依賴。

我們使用DIP設計原則為V抽象一個介面IVforY,並把該介面放在B包中。
這樣就把Y對V的呼叫轉化為:
V繼承IVforY
Y呼叫IVforY
如圖:


這樣一來,包B中的類就不依賴任何包A中的類了。

小結

無環依賴原則(ADP)為我們解決包之間的關係耦合問題。在設計包結構時,不能有迴圈依賴。

8. The Stable Dependencies Principle (SDP) - 穩定依賴原則

一個設計中的包之間的依賴應該朝著穩定的方向進行。一個包只應該依賴那些比自己更穩定的包。換成另一個說法是: 朝著穩定的方向進行依賴。包之間的關係方面(耦合性)的原則。

包的依賴

如果一個包A 中的類引用了包B中的類,我們稱包A依賴包B。
“依賴”在具體的程式語言中表現為,如果A依賴B,C/C++語言則在A包的檔案/類中通過#include語句包含B包中的檔案/類;Java語言則A包的類中通過import語句引入B包中的類。

圖1(包A依賴包B)
虛線表示一種依賴關係,箭頭表示依賴的方向,箭頭所在的一側就是被依賴的包。

包的穩定依賴原則

包應該依賴比自己更穩定的包。因為如果依賴一個不穩定的包,那麼當這個不穩定的包發生變化時,本身穩定的包也不得不發生變化,變得不穩定了。

所謂穩定,在現實生活中是指一個物體具有穩固不變的屬性使它很難發生變化。應用到軟體概念上,我們認為一個軟體是穩定的,是因為這個軟體很難發生改變,或更確切地說,是不需要發生改變。一個設計良好,能應對各種變化不需要修改的軟體當然是穩定的了,但事實上,往往一個軟體常常需要對應某個事先沒有預測到的使用者需求而不得不發生改變,當這種改變發生時,能把修改控制在最小的範圍之內,並能穩定的工作(包括軟體本身以及依賴它的其它軟體實體等),我們也會認為該軟體是相對穩定的。

怎麼樣讓一個軟體足夠穩定呢?一個確切的方法是,讓大量其它軟體的包依賴它。一個包被很多其他包依賴是非常穩定的,這是因為被依賴的包為了協調其他包必須做很多的工作來對應各種變化(責任的負擔者)。

圖1:穩定的包X
我們認為X是穩定的,因為:
- X被很多其他包依賴。相當於責任擔當著。
- X沒有依賴別的包,它是獨立的。

相反,下面列出了一個非常不穩定的包Y,如圖:


圖2:不穩定的包Y
我們認為Y是不穩定的,因為:
- Y沒有被其他的包所依賴。不是責任的擔當著。
- Y依賴很多別的包。

包的穩定性的判斷原則

可以通過下面的方法來判斷一個包的穩定係數:
Ca:Afferent Coupling。向心耦合。依賴該包(包含的類)的外部包(類)的數目(i.e. incoming dependencies)。
Ce: Efferent Coupling。離心耦合。被該包依賴的外部包的數目(i.e. outgoing dependencies)。
I: Instability。不穩定性。I=Ce/(Ce+Ca)。它的值處於[0,1]之間。
如圖1,X的Ce=0,所以不穩定性I=0,它是穩定的。相反,如圖2,Y的Ce=3,Ca=0,所以它的不穩定性I=1,它是不穩定的。

SDP要求一個包的不穩定性I要大於它所依賴的包的不穩定性。換句話說,沿著依賴的方向,包的不穩定性應該逐漸降低,穩定性應該逐漸升高。

穩定依賴原則(SDP)為我們解決包之間的關係耦合問題。在設計包結構時,包應該只依賴比自己更穩定的包。

9.  The Stable AbstractionsPrinciple (SAP) - 穩定抽象等價原則

最穩定的包應該是最抽象的包。不穩定的包應該是具體的包。包的抽象程度跟它的穩定性成正比。穩定的包應該是抽象的包。

包的穩定抽象等價原則


我們在The Stable Dependencies Principle (SDP) - OO設計的穩定依賴原則 一文中談到了包的穩定性:不容易改變的包應該具有更好的穩定性。

一個包的抽象程度越高,它的穩定性就越高。反之,它的穩定性就越低。一個穩定的包必須是抽象的,反之,不穩定的包必須是具體的。

穩定的包的構成
抽象類或介面通過子類繼承擴充套件行為,這表示抽象類或介面比它們的子類更具有穩定性。總之,為了構成穩定的包,應該提高包內的抽象類或介面的比率;它們的子類可以放在另一個不穩定的包內,該包依賴上述穩定的包,從而遵循了穩定依賴原則(SDP)。

理想的體系結構應該是:
不穩定的(容易改變的)包處於上層

- 它們是具體的包實現
穩定的(不容易改變的)包處於下層
- 不容易改變,但容易擴充套件
- 介面比實現(具體的執行程式碼)在內在特性上更具有穩定性

圖1:遵循穩定依賴原則(SDP)的理想的體系結構

小結

穩定抽象等價原則(SAP)為我們解決包之間的關係耦合問題。在設計包結構時,穩定的包應該是抽象的(由抽象類或介面構成),不穩定的包應該是具體的(由具體的實現類構成)。

10.  The Common ClosurePrinciple (CCP) - 共同封閉原則


一個包中所有的類應該對同一種類型的變化關閉。一個變化影響一個包,便影響了包中所有的類。一個更簡短的說法是:一起修改的類,應該組合在一起(同一個包裡)。包的內部關係方面(聚合性)的原則

如果必須修改應用程式裡的程式碼,我們希望所有的修改都發生在一個包裡(修改關閉),而不是遍佈在很多包裡。
CCP原則就是把因為某個同樣的原因而需要修改的所有類組合進一個包裡。如果2個類從物理上或者從概念上聯絡得非常緊密,它們通常一起發生改變,那麼它們應該屬於同一個包。

CCP跟開閉原則(OCP: Open Closed Principle) 有著很深的淵源關係,CCP的“關閉”(closure)就是OCP所提倡的:classes should be closed for modification but open for extension. 類應該對修改關閉,對擴充套件開放。但我們知道,100%的“關閉”是不現實的,我們在設計系統時,只能儘量地保持對大多數可預見的修改關閉。
CCP延伸了OCP的“關閉”概念,當因為某個原因需要修改時,把需要修改的範圍限制在一個最小範圍內的包裡。CCP原則幫助我們決定哪些類應該被放到同一個包裡。

小結

共同封閉原則(CCP)從軟體功能的角度上為我們規範了包設計的一個原則:在設計包時,相互之間緊密關聯的類應該放在同一包裡。

11. The Common Reuse Principle (CRP) - 全部重用原則

包的所有類被一起重用。如果你重用了其中的一個類,就重用全部。換成另一個比較淺顯易懂的說法:沒有被一起重用的類不應該被組合在一起。CRP原則幫助我們決定哪些類應該被放到同一個包裡。包的內部關係方面(聚合性)的原則

依賴一個包就是依賴這個包所包含的一切。當一個包發生了改變,併發布新的版本,使用這個包的所有使用者都必須在新的包環境下驗證他們的工作,即使被他們使用的部分沒有發生任何改變。
因為如果包中包含有未被使用的類,即使使用者不關心該類是否改變,但使用者還是不得不升級該包並對原來的功能加以重新測試。

“不能強迫使用者去依賴那些他們不使用的介面”,把這個概念應用到更廣範圍的包上,就是CRP的基本理念:不要把使用者不使用的類一起組合進包裡。

CRP與REP一樣,都是從方便使用者重用的角度去設計包,重用者是他們的受益者,CCP則讓系統的維護者受益。CCP讓包儘可能大(CCP原則加入功能相關的類),CRP則讓包儘可能小(CRP原則剔除不使用的類)。它們的出發點不一樣,但不相互衝突。CRP保證了包的內部具有很高的聚合性。

全部重用原則(CRP)從使用者的角度上為我們規範了包設計的一個原則:在設計包時,相互之間沒有緊密關聯的類不應該放在同一包裡。

12.  Least Knowledge Principle (LKP) -迪米特法則

迪米特法則(Law of Demeter,LoD)也稱為最少知識原則(Least Knowledge Principle,LKP)。

一個物件應該對其他物件有最少的瞭解。通俗地講,一個類應該對自己需要耦合或呼叫的類知道得最少,你(被耦合或呼叫的類)的內部是如何複雜都和我沒關係,那是你的事情,我就知道你提供的public方法,我就呼叫這麼多,其他的一概不關心。

含義:

  • 只和朋友交流

朋友類的定義是這樣的:出現在成員變數、方法的輸入輸出引數中的類稱為成員朋友類,而出現在方法體內部的類不屬於朋友類。下面的程式碼在方法體內部依賴了其他類,這嚴重違反迪米特法則

01

public class Teacher {

02

03

    public void commond(GroupLeader groupLeader) {

04

        List<Girl> listGirls = new ArrayList<Girl>();

05

06

        for (int i = 0; i < 20; i++) {

07

            listGirls.add(new Girl());

08

        }

09

10

        groupLeader.countGirls(listGirls);

11

    }

12

13

}

方法是類的一個行為,類竟然不知道自己的行為與其他類產生了依賴關係,這是不允許的。

正確的做法是:

1

public class Teacher {

2

3

    public void commond(GroupLeader groupLeader) {

4

        groupLeader.countGirls();

5

    }

6

7

}

01

public class GroupLeader {

02

03

    private List<Girl> listGirls;

04

05

    public GroupLeader(List<Girl> _listGirls) {

06

        this.listGirls = _listGirls;

07

    }

08

09

    public void countGirls() {

10

        System.out.println("女生數量是:" + listGirls.size());

11

    }

12

13

}

注意:一個類只和朋友交流,不與陌生類交流,不要出現getA().getB().getC().getD()這種情況(在一種極端情況下允許出現這種訪問,即每一個點號後面的返回型別都相同),類與類之間的關係是建立在類間的,而不是方法間,因此一個方法儘量不引入一個類中不存在的物件,當然,JDK API提供的類除外。

  • 朋友間也是有距離的

一個類公開的public屬性或方法越多,修改時涉及的面也就越大,變更引起的風險擴散也就越大。因此,為了保持朋友類間的距離,在設計時需要反覆衡量:是否還可以再減少public方法和屬性,是否可以修改為private、package-private(包型別,在類、方法、變數前不加訪問許可權,則預設為包型別)、protected等訪問許可權,是否可以加上final關鍵字等。

注意:迪米特法則要求類“羞澀”一點,儘量不要對外公佈太多的public方法和非靜態的public變數,儘量內斂,多使用private、package-private、protected等訪問許可權。

  • 是自己的就是自己的

如果一個方法放在本類中,既不增加類間關係,也對本類不產生負面影響,就放置在本類中。

  • 謹慎使用Serializable

最後,迪米特法則的核心觀念就是類間解耦,弱耦合,只有弱耦合了以後,類的複用率才可以提高。

Principle –縱覽


----類原則 ----
1.單一職責原則 -Single Responsibility Principle(SRP)
就一個類而言,應該僅有一個引起它變化的原因。
(職責即為“變化的原因”。)
2.開放-封閉原則 - OpenClose Principle(OCP)
軟體實體(類、模組、函式等)應該是可以擴充套件的,但是不可修改。
(對於擴充套件是開放的,對於更改是封閉的.
關鍵是抽象.將一個功能的通用部分和實現細節部分清晰的分離開來.
開發人員應該僅僅對程式中呈現出頻繁變化的那些部分作出抽象.
拒絕不成熟的抽象和抽象本身一樣重要. )
3.里氏替換原則 -Liskov Substitution Principle(LSP)
子型別(subclass)必須能夠替換掉它們的基型別(superclass)。
4.依賴倒置原則(IoCP) 或 依賴注入原則 -Dependence Inversion Principle(DIP)
抽象不應該依賴於細節。細節應該依賴於抽象。
(Hollywood原則: "Don't call us, we'll call you".
程式中所有的依賴關係都應該終止於抽象類和介面。
針對介面而非實現程式設計。
任何變數都不應該持有一個指向具體類的指標或引用。
任何類都不應該從具體類派生。
任何方法都不應該覆寫他的任何基類中的已經實現了的方法。)
5.介面隔離原則(ISP)
不應該強迫客戶依賴於它們不用的方法。
介面屬於客戶,不屬於它所在的類層次結構。
(多個面向特定使用者的介面勝於一個通用介面。)


----包內聚原則 ----
6.重用釋出等價原則(REP)
重用的粒度就是釋出的粒度。
7.共同封閉原則(CCP)
包中的所有類對於同一類性質的變化應該是共同封閉的。
一個變化若對一個包產生影響,
則將對該包中的所有類產生影響,
而對於其他的包不造成任何影響。
8.共同重用原則(CRP)
一個包中的所有類應該是共同重用的。
如果重用了包中的一個類,
那麼就要重用包中的所有類。
(相互之間沒有緊密聯絡的類不應該在同一個包中。)
----包耦合原則
9.無環依賴原則(ADP)
在包的依賴關係圖中不允許存在環。
10.穩定依賴原則(SDP)
朝著穩定的方向進行依賴。
應該把封裝系統高層設計的軟體(比如抽象類)放進穩定的包中,
不穩定的包中應該只包含那些很可能會改變的軟體(比如具體類)。
11.穩定抽象原則(SAP)
包的抽象程度應該和其穩定程度一致。
(一個穩定的包應該也是抽象的,一個不穩定的包應該是抽象的. )


----其它擴充套件原則----
12.BBP(Black Box Principle)黑盒原則
多用類的聚合,少用類的繼承。
13.DAP(Default Abstraction Principle)預設抽象原則
在介面和實現介面的類之間引入一個抽象類,這個類實現了介面的大部分操作.
14.IDP(Interface Design Principle)介面設計原則
規劃一個介面而不是實現一個介面。
15.DCSP(Don't Concrete Supperclass Principle)不要構造具體的超類原則
避免維護具體的超類。
16.迪米特法則
一個類只依賴其觸手可得的類。