OOD設計基本原則

OOD 設計基本原則

 

Ø       OCP 原則

Ø       里氏替換原則

Ø       依賴倒置原則

Ø       接口隔離原則

Ø       聚合與繼承原則

Ø       單一職責原則

Ø       Separation of concerns Principle

Ø       Pareto Principle ( 帕雷多原則 80/20 原則 )

 

 

OOD 設計原則在提高一個系統可維護性的同時, 提高這個系統的可複用性. 他們是一些指導原則, 依照這些原則設計, 我們就可以有效的提高系統的複用性, 同時提高系統的可維護性.

OCP 原則

Open-Closed Principle

 

這些OOD 原則的一個基石就是"- 閉原則"(Open-Closed Principle OCP). 這個原則最早是由Bertrand Meyer 提出, 英文的原文是:Software entities should be open for extension, but closed for modification. 意思是說, 一個軟件實體應當對擴展開放, 對修改關閉. 也就是說, 我們在設計一個模塊的時候, 應當使這個模塊可以在不被修改的前提下被擴展, 換句話說就是, 應當可以在不必修改源代碼的情況下改變這個模塊的行爲.

滿足OCP 的設計給系統帶來兩個無可比擬的優越性.

Ø       通過 擴展已有的軟件系統, 可以提供新的行爲, 以滿足對軟件的新需求, 使變化中的軟件系統有一定的適應性和靈活性 .

Ø       已有的軟件模塊, 特別是最重要的抽象層模塊不能再修改, 這就使變化中的軟件系統有一定的穩定性和延續性 .

具有這兩個優點的軟件系統是一個高層次上實現了複用的系統, 也是一個易於維護的系統. 那麼, 我們如何才能做到這個原則呢? 不能修改而可以擴展, 這個看起來是自相矛盾的. 其實這個是可以做到的, 按面向對象的說法, 這個就是不允許更改系統的抽象層, 而允許擴展的是系統的實現層 .

解決問題的關鍵在: 抽象化 . 我們讓模塊依賴於一個固定的抽象體, 這樣它就是不可以修改的; 同時, 通過這個抽象體派生, 我們就可以擴展此模塊的行爲功能. 如此, 這樣設計的程序只通過增加代碼來變化而不是通過更改現有代碼來變化, 前面提到的修改的副作用就沒有了.

" -" 原則如果從另外一個角度講述, 就是所謂的" 對可變性封裝原則 "(Principle of Encapsulation of Variation, EVP ). 講的是找到一個系統的可變因素, 將之封裝起來. 在我們考慮一個系統的時候, 我們不要把關注的焦點放在什麼會導致設計發生變化上, 而是考慮允許什麼發生變化而不讓這一變化導致重新設計. 也就是說, 我們要積極的面對變化, 積極的包容變化, 而不是逃避.

  

[SHALL01] 將這一思想用一句話總結爲:" 找到一個系統的可變因素, 將它封裝起來 ", 並將它命名爲" 對可變性的封裝原則"." 對可變性的封裝原則" 意味者兩點:

Ø       一種 可變性應當被封裝到一個對象裏面 , 而不應當散落到代碼的很多角落裏面 . 同一種可變性的不同 表象意味着同一個繼承等級結構中的具體子類. 繼承應當被看做是封裝變化的方法 , 而不應當是被認爲從一般的對象生成特殊的對象的方法( 繼承經常被濫用).

Ø       一種可變性不應當與另外一種可變性混合在一起. 從具體的類圖來看, 如果繼承結構超過了兩層, 那麼就意味着將兩種不同的可變性混合在了一起.

" 對可變性的封裝原則" 從工程的角度說明了如何實現OCP. 如果按照這個原則來設計, 那麼系統就應當是遵守OCP. 但是現實往往是殘酷的, 我們不可能100% 的遵守OCP, 但是我們要向這個目標來靠近. 設計者要對設計的模塊對何種變化封閉做出選擇.

 

里氏替換原則

Liskov Substitution Principle

 

從上一篇的"-" 原則中可以看出, 面向對象設計的重要原則是創建抽象化, 並且從抽象化導出具體化 . 這個導出要使用繼承關係和一個原則: 里氏替換原則(Liskov Substitution Principle, LSP).
  

那麼什麼是里氏替換原則呢? 有個嚴格的表述, 繞口, 不好記. 還是比較白話的這個好記. 說的是: 一個軟件實體如果使用的是一個基類的話, 那麼一定適用於其子類, 而且它察覺不出基類對象和子類對象的區別 . 也就是說, 在軟件裏面, 把基類都替換成它的子類, 程序的行爲沒有變化.
  

LSP 是繼承複用的基石, 只有當衍生類可以替換掉基類, 軟件單位的功能不受到影響時, 基類才能真正被複用, 而衍生類也能夠在基類的基礎上增加新的行爲.
  

下面, 我們從代碼重構的角度來對LSP 進行理解.LSP 講的是基類和子類的關係. 只有當這種關係存在時, 里氏替換關係才存在. 如果兩個具體的類A,B 之間的關係違反了LSP 的設計,( 假設是從BA 的繼承關係) 那麼根據具體的情況可以在下面的兩種重構方案中選擇一種.
  

Ø       創建一個新的抽象類C, 作爲兩個具體類的超類,A,B 的共同行爲移動到C 中來解決問題.BA 的繼承關係改爲委派關係. 爲了說明, 我們先用第一種方法來看一個例子。第二種辦法在另外一個原則中說明. 我們就看那個著名的長方形和正方形的例子. 對於長方形的類, 如果它的長寬相等, 那麼它就是一個正方形, 因此, 長方形類的對象中有一些正方形的對象. 對於一個正方形的類, 它的方法有個setSidegetSide, 它不是長方形的子類, 和長方形也不會符合LSP.
  

那麼, 如果讓正方形當做是長方形的子類, 會出現什麼情況呢? 我們讓正方形從長方形繼承, 然後在它的內部設置width 等於height, 這樣, 只要width 或者height 被賦值, 那麼widthheight 會被同時賦值, 這樣就保證了正方形類中,widthheight 總是相等的. 現在我們假設有個客戶類, 其中有個方法, 規則是這樣的, 測試傳人的長方形的寬度是否大於高度, 如果滿足就停止下來, 否則就增加寬度的值. 現在我們來看, 如果傳人的是基類長方形, 這個運行的很好. 根據LSP, 我們把基類替換成它的子類, 結果應該也是一樣的, 但是因爲正方形類的widthheight 會同時賦值, 這個方法沒有結束的時候, 條件總是不滿足, 也就是說, 替換成子類後, 程序的行爲發生了變化, 它不滿足LSP.
  

那麼我們用第一種方案進行重構, 我們構造一個抽象的四邊形類, 把長方形和正方形共同的行爲放到這個四邊形類裏面, 讓長方形和正方形都是它的子類, 問題就OK. 對於長方形和正方形,widthheight 是它們共同的行爲, 但是給widthheight 賦值, 兩者行爲不同, 因此, 這個抽象的四邊形的類只有取值方法, 沒有賦值方法. 上面的例子中那個方法只會適用於不同的子類,LSP 也就不會被破壞.
  

在進行設計的時候, 我們儘量從抽象類繼承, 而不是從具體類繼承. 如果從繼承等級樹來看, 所有葉子節點應當是具體類, 而所有的樹枝節點應當是抽象類或者接口. 當然這個只是一個一般性的指導原則, 使用的時候還要具體情況具體分析.

 

依賴倒置原則

Dependency-Inversion Principles

" - " 原則是我們 OOD 的目標 , 達到這一目標的主要機制就是 " 依賴倒轉原則 ". 這個原則的內容是 : 要依賴於抽象 , 不要依賴於具體 .

 

對於抽象層 次來說 , 它是一個系統的本質的概括 , 是系統的商務邏輯和宏觀的 , 戰略性的決定 , 是必然性的體現 ; 具體的層次 則是與實現有關的算法和邏輯 , 一些戰術性的決定 , 帶有相當大的偶然性 . 傳統的過程性系統設計辦法傾向於使高層次的模塊依賴於低層次的模塊 ; 抽象層次依賴於具體層次 . 這實際上就是微觀決定宏觀 , 戰術決定戰略 , 偶然決定必然 . 依賴倒轉原則就是要把這種錯誤的依賴關係倒轉過來 .

 

許多的建構設計模型 , 例如 COM, CORBA, JavaBean, EJB , 它們背後的基本原則就是 DIP.

     

對於軟件設計的兩個目標 , 複用和可維護性 來說 , 傳統的設計側重於具體層次模塊的複用和可維護 , 比如算法 , 數據結構 , 函數庫等等 . 但是 , 對系統的抽象是比較穩定的 , 它的複用是很重要的 , 同時 , 抽象層次的可維護性也應當是一個重點 . 就是說 DIP 也導致複用和可維護性的 " 倒轉 ".

      

我們現在來看看依賴有幾種 , 依賴也就是耦合 , 分爲下面三種

Ø       零耦合 (Nil Coupling) 關係 , 兩個類沒有依賴關係 , 那就是零耦合

Ø       具體耦合 (Concrete Coupling) 關係 , 兩個具體的類之間有依賴關係 , 那麼就是具體耦合關係 , 如果一個具體類直接引用另外一個具體類 , 就會發生這種關係 .

Ø       抽象耦合 (Abstract Coupling) 關係 . 這種關係發生在一個具體類和一個抽象類之間 , 這樣就使必鬚髮生關係的類之間保持最大的靈活性 .

     

DIP 要求客戶端依賴於抽象耦合 , 抽象不應當依賴於細節 , 細節應當依賴於抽象 (Abstractions should not depend upon details. Details should depend upon abstractions), 這個原則的另外一個表述就是 " 四人團 " 強調的那個 : 要針對接口編程 , 不要對實現編程 .(Program to an interface, not an implementation), 程序在需要引用一個對象時 , 應當儘可能的使用抽象類型作爲變量的靜態類型 , 這就是針對接口編程的含義 . DIP 是達到 " - " 原則的途徑 .

 

要做到 DIP, 用抽象方式耦合是關鍵 . 由於一個抽象耦合總要涉及具體類從抽象類繼承 . 並且需要保證在任何引用到某類的地方都可以改換成其子類 , 因此 ,LSP DIP 的基礎 .DIP OOD 的核心原則 , 設計模式的研究和應用都是用它作爲指導原則的 .DIP 雖然強大 , 但是也很難實現 . 另外 ,DIP 是假定所有的具體類都會變化 , 這也不是全對 , 有些具體類就相當穩定 . 使用這個類的客戶端就完全可以依賴這個具體類而不用再弄一個抽象類 .

 

接口隔離原則

Interface Segregation Principle

 

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

這裏的 " 接口 " 往往有兩種不同的含義 :

Ø       一種是指一個類型所具有的方法特徵的集合 , 僅僅是一種邏輯上的抽象 ;

Ø       另外一種是指某種語言具體的 " 接口 " 定義 , 有嚴 格的定義和結構 . 比如 Java 語言裏面的 Interface 結構 .

對於這兩種不同的含義 ,ISP 的表達方式以及含義都有所不同 .( 上面說的一個類型 , 可以理解成一個類 , 我們定義了一個類 , 也就是定義了一種新的類型 )

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

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

在我們進行 OOD 的時候 , 一個重要的工作就是恰當的劃分角色和角色對應的接口 . 將沒有關係的接口合併在一起 , 是對角色和接口的 污染 . 如果將一些看上去差不多的接口合併 , 並認爲這是一種代碼優化 , 這是錯誤的 . 不同的角色應該交給不同的接口 , 而不能都交給一個接口 .

 

對於定製服務 , 這樣做最大的好處就是系統的可維護性 . 向客戶端提供接口是一種承諾 ,public 接口後是不能改變的 , 因此不必要的承諾就不要做出 , 承諾越少越好 .

聚合與繼承原則

 

合成 Composition )和聚合 Aggregation )都是關聯 Association )的特殊種類。

聚合表示整體和部分的關係,表示 擁有 ;合成則是一種更強的 擁有 ,部分和整體的生命週期一樣。合成的新的對象完全支配其組成部分,包括它們的創建和湮滅等。一個合成關係的成分對象是不能與另一個合成關係共享的。換句話說,合成是值的聚合( Aggregation by Value ),而一般說的聚合是引用的聚合( Aggregation by Reference )。

 

簡短的說,合成-聚合複用原則( CARP )是指,儘量使用合成 / 聚合,而不是使用繼承
OOD 中,有兩種基本的辦法可以實現複用,一種是通過合成 / 聚合,另外一種就是通過繼承

 

通過合成 / 聚合 的好處是:

Ø       新對象存取成分對象的唯一方法是通過成分對象的接口。

Ø       這種複用是黑箱複用,因爲成分對象的內部細節是新對象所看不見的。

Ø       這種複用支持包裝。

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

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

Ø       這種複用可以在運行時間內動態進行,新對象可以動態的引用與成分對象類型相同的對象。

Ø       作爲複用手段可以應用到幾乎任何環境中去。

Ø       它的缺點 就是系統中會有較多的對象需要管理。
   

通過繼承 來進行復用的優點是:

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

Ø       修改和擴展繼承而來的實現較爲容易。

缺點 是:

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

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

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

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

 

如何選擇?

要正確的選擇合成 / 複用和繼承,必須透徹的理解里氏替換原則 Coad 法則 。里氏替換原則前面學習過, Coad 法則由 Peter Coad 提出,總結了一些什麼時候使用繼承作爲複用工具的條件。只有當以下的 Coad 條件全部被滿足時,才應當使用繼承關係:

Ø       子類是超類的一個特殊種類,而不是超類的一個角色,也就是區分 “Has-A” “Is-A” 。只有 “Is-A” 關係才符合繼承關係, “Has-A” 關係應當用聚合來描述。

Ø       永遠不會出現需要將子類換成另外一個類的子類的情況。如果不能肯定將來是否會變成另外一個子類的話,就不要使用繼承。

Ø       子類具有擴展 超類的責任,而不是具有置換 調( override )或註銷 掉( Nullify )超類的責任。如果一個子類需要大量的置換掉超類的行爲,那麼這個類就不應該是這個超類的子類。

Ø       只有在分類學角度上有意義時,纔可以使用繼承。不要從工具類繼承。

 

錯誤的使用繼承而不是合成 / 聚合的一個常見原因是錯誤的把 “Has-A” 當成了 “Is A” “Is A” 代表一個類是另外一個類的一種; “Has-A” 代表一個類是另外一個類的一個角色,而不是另外一個類的特殊種類。
我們看一個例子。如果我們把 當成一個類,然後把 僱員 經理 學生 當成是 的子類。這個的錯誤在於把 角色 的等級結構和 的等級結構混淆了。 經理 僱員 學生 是一個人的角色,一個人可以同時擁有上述角色。如果按繼承來設計,那麼如果一個人是僱員的話,就不可能是經理,也不可能是學生,這顯然不合理。正確的設計是有個抽象類 角色 可以擁有多個 角色 (聚合), 僱員 經理 學生 角色 的子類。
另外一個就是隻有兩個類滿足里氏替換原則的時候,纔可能是 “Is A” 關係。也就是說,如果兩個類是 “Has-A” 關係,但是設計成了繼承,那麼肯定違反里氏替換原則。

 

單一職責原則

Single Responsibility Principle (SRP)

 

就一個類而言,應該只專注於做一件事和僅有一個引起它變化的原因。

 

所謂職責,我們可以理解他爲功能,就是設計的這個類功能應該只有一個,而不是兩個或更多。也可以理解爲引用變化的原因,當你發現有兩個變化會要求我們修改這個類,那麼你就要考慮撤分這個類了。因爲職責是變化的一個軸線,當需求變化時,該變化會反映類的職責的變化。

“就像一個人身兼數職,而這些事情相互關聯不大,,甚至有衝突,那他就無法很好的解決這些職責,應該分到不同的人身上去做纔對。”

 

二、舉例說明:

違反 SRP 原則代碼 :

modem 接口明顯具有兩個職責:連接管理和數據通訊;

interface Modem

{

    public void dial(string pno);

    public void hangup();

    public void send(char c);

    public void recv();

}

 

如果應用程序變化影響連接函數,那麼就需要重構:

interface DataChannel

{

    public void send(char c);

    public void recv();

}

interface Connection

{

    public void dial(string pno);

    public void hangup();

}

 

三、 SRP 優點:

消除耦合,減小因需求變化引起代碼僵化性臭味

 

四、使用 SRP 注意點:

1 、一個合理的類,應該僅有一個引起它變化的原因,即單一職責;

2 、在沒有變化徵兆的情況下應用 SRP 或其他原則是不明智的;

3 、在需求實際發生變化時就應該應用 SRP 等原則來重構代碼;

4 、使用測試驅動開發會迫使我們在設計出現臭味之前分離不合理代碼;

5 、如果測試不能迫使職責分離,僵化性和脆弱性的臭味會變得很強烈,那就應該用 Facade Proxy 模式對代碼重構;

 

Separation of concerns Principle

In computer science , separation of concerns (SoC ) is the process of breaking a computer program into distinct features that overlap in functionality as little as possible . A concern is any piece of interest or focus in a program. Typically, concerns are synonymous with features or behaviors. Progress towards SoC is traditionally achieved through modularity and encapsulation , with the help of information hiding . Layered designs in information systems are also often based on separation of concerns (e.g., presentation layer, business logic layer, data access layer, database layer).

All programming paradigms aid developers in the process of improving SoC. For example, object-oriented programming languages such as C++ , Java , and C# can separate concerns into objects , and a design pattern like MVC can separate content from presentation and data-processing (model) from content. Service-oriented design can separate concerns into services . Procedural programming languages such as C and Pascal can separate concerns into procedures . Aspect-oriented programming languages can separate concerns into aspects and objects .

Separation of concerns is an important design principle in many other areas as well, such as urban planning , architecture and information design . The goal is to design systems so that functions can be optimized independently of other functions , so that failure of one function does not cause other functions to fail, and in general to make it easier to understand, design and manage complex interdependent systems. Common examples include using corridors to connect rooms rather than having rooms open directly into each other, and keeping the stove on one circuit and the lights on another.

Pareto Principle ( 帕雷多原則 80/20 原則 )

 

1960 年意大利經濟學家帕雷多建立了一個數學模型來描述國家不平等的財富分配,發現 20% 的人擁有了財富的 80%

在帕雷多經過觀察並建立了模型之後,許多人都在他們各自的領域發現了同樣的現象。

關於 重要的少數和普遍的多數 的發現,即 20% 因素往往決定事物 80% 的結果變成了有名的帕雷多定律或者 80/20 原則。

80/20 原則的含義是一切事物都是這樣組成的: 20% 是至關重要的,而 80% 是平常的。

80/20 原則在軟件開發領域的體現:

1 )軟件開發中 20% 的功能是用戶經常使用的, 80% 的功能其實沒有那麼重要;

2 )軟件框架的設計能滿足 80% 的應用開發, 20% 可能並不適用;在架構設計中要意思到不能滿足所有的情況,同時考慮特殊情況的特殊處理;

3 )軟件開發團隊中非核心成員和核心成員也體現了 80/20 原則;

4 80% 的軟件 Bug 集中在 20% 的軟件模塊中,體現了重點核心模塊的重點開發和維護。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章