轉java設計原則

類的設計原則

依賴倒置原則-Dependency Inversion Principle (DIP)
里氏替換原則-Liskov Substitution Principle (LSP)
接口分隔原則-Interface Segregation Principle (ISP)
單一職責原則-Single Responsibility Principle (SRP)
開閉原則-The Open-Closed Principle (OCP)
一. 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來組合關係。


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

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

A 不應該在代碼中出現if/else之類對子類類型進行判斷的條件。以下代碼就違反LSP定義。


1
if (obj typeof Class1) {
2
do something
3
} else if (obj typeof Class2) {
4
do something else
5
}
B 子類應當可以替換父類並出現在父類能夠出現的任何地方,或者說如果我們把代碼中使用基類的地方用它的子類所代替,代碼還能正常工作。

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

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

05
public double getHeight() {
06
return height;
07
}
08

09
public void setHeight(double height) {
10
this.height = height;
11
}
12

13
public double getWidth() {
14
return width;
15
}
16

17
public void setWidth(double width) {
18
this.width = width;
19
}
20
}
01
class Square extends Rectangle {
02
public void setHeight(double height) {
03
super.setHeight(height);
04
super.setWidth(height);
05
}
06

07
public void setWidth(double width) {
08
super.setHeight(width);
09
super.setWidth(width);
10
}
11
}
這裏Rectangle是基類,Square從Rectangle繼承。這種繼承關係有什麼問題嗎?

假如已有的系統中存在以下既有的業務邏輯代碼:

1
void g(Rectangle r) {
2
r.setWidth(5);
3
r.setHeight(4);
4
if (r.getWidth() * r.getHeight() != 20) {
5
throw new RuntimeException();
6
}
7
}
則對應於擴展類Square,在調用既有業務邏輯時:

1
Rectangle square = new Square();
2
g(square);
時會拋出一個RuntimeException異常。這顯然違反了LSP原則。

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

Design by Contract

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

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


三. 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)從對接口的使用上爲我們對接口抽象的顆粒度建立了判斷基準:在爲系統設計接口的時候,使用多個專門的接口代替單一的胖接口。


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


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

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

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


職責的劃分

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

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

SRP違反例:
Modem.java
1
interface Modem {
2
public void dial(String pno); //撥號
3
public void hangup(); //掛斷
4
public void send(char c); //發送數據
5
public char recv(); //接收數據
6
}
咋一看,這是一個沒有任何問題的接口設計。但事實上,這個接口包含了2個職責:第一個是連接管理(dial, hangup);另一個是數據通信(send, recv)。很多情況下,這2個職責沒有任何共通的部分,它們因爲不同的理由而改變,被不同部分的程序調用。
所以它違反了SRP原則。

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

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

小結

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

五. 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%滿足開閉原則的軟件系統是相當困難的,這就是開閉原則的相對性。但在設計過程中,通過對模塊功能的抽象(接口定義),模塊之間的關係的抽象(通過接口調用),抽象與實現的分離(面向接口的程序設計)等,可以儘量接近滿足開閉原則。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章