OO原則
前言:設計時需要考慮這些原則,但隨意使用這些原則會使你的程序出現不必要的複雜性(Needless Complexity)。
參考及說明
本文的撰寫,是基於閱讀後的梳理
《Agile Software Development:Principles,Patterns.and.Practices 》
1. 開放-封閉原則(Open-Closed Principle)
簡稱OCP。
(一)概念:
軟件實體(類、模塊、函數等等)應該是可以拓展的,但是不可修改。
即對擴展開發,對修改封閉。
(二)開發封閉體現何處?
這點我沒找到書上的解釋,我的理解是,開發封閉是對信息的控制。
如果可以控制在基類層面中的信息,就不要對派生類開發。
如果可以控制在模塊內部的信息,就不要對外部暴露。
(三)何時修改
有意思的是,在軟件系統的模塊設計中,並不能一下子做到模塊在之後需求的變化中始終能夠保持對修改的封閉。修改設計可以說是不可避免的,但過分設計顯然在時間成本和複雜性上都是不明智的。
我很贊成書上的觀點:
p.98
只受一次愚弄。
最初編寫代碼時,假設變化不會發生。當變化發生時,我們就創建抽象隔離以後發生的同類變化。
(四)獲取封閉性的方法
- 使用抽象獲取封閉性
通過一個典型的例子Shape的可以很容易理解
但這個設計有個缺點就是,當頻繁地創建Shape派生類時,會體現出弊端。
得進一步藉助Acyclic Vistor模式來解決弊端。
- 使用“數據驅動”獲取封閉性
比如表格機制,類似配置文件的方式。
(五)後果
如果未考慮充分這個原則,會導致程序僵化性(Rigidity)。
2. 單一職責原則(SRP)
(一)概念:
就一個類而言,應該僅有一個引起它變化的原因。
(二)爲什麼要遵循單一職責原則?
我覺得書上這段說有道理:
p.88
每一個職責都是變化的一個軸線(an axis of change)。當需求變化時,該變化會反應爲類的職責的變化。如果一個類承擔了多個職責,那麼引起它變化的原因就會有多個。
所以當需求變化頻繁,或系統足夠複雜時,遵循單一職責,分離職責有助於系統在適應需求時仍保持靈活性。
(三)職責 定義的角度
這時候,我對內聚性(一個模塊組成元素之間的功能相關性)和職責之間的關係思考了一番。我覺得這句話很關鍵:
p.89
SRP中,我們把職責定義爲“變化的原因”。
這顯然是從因果的角度去定義的。
SRP是最基本的原則之一,也是最難運用的之一。從面向對象分析的角度,我們自然地會將職責結合在一起。但我們需要從需求變化的角度去重新審視可能的變化。
(四)如何判斷兩個職責是否應該被分開?
依賴於應用程序變化的方式。
如果應用程序導致兩個職責同時變化,那麼就不必分離它們。
變化的軸線僅當實際發生變化時才具有真正的意義。如果沒有變化的徵兆,應用SRP是不明智的。
我覺得這個變化的徵兆,正是系統拓展的潛在可能。甚至在需求分析階段,就得意識到這種可能的變化。
3. Liskov原則(LSP)
(一)概念:
子類型(subtype)必須能夠替換掉它們的基類型。
這點我就直接貼出來效果更好:
可見,子類型並不是從現實生活中去揣摩,之前java入門對子類的理解太過於形象,有失嚴謹,現在上升到設計層面了,得立即糾正觀念。
我覺得LSP的出現,是對設計的繼承體系模型的一種考驗。
(二)判斷繼承體系是否遵循LSP
這幾乎是個難點,書上舉的例子在違背LSP時,都很隱晦,比如正方形與長方形等。建議看書第10章。
有時我們雖然設計出了一個自認爲“合理”的模型(繼承體系),然而正如書上所說:
p.107
一個模型如果孤立的看,並不具備真正意義上的有效性。模型的有效性只能通過客戶程序來體現。
客戶程序在拿到類時,在調用時,會有一些前置條件和後置條件的條件,這些條件構成了“契約”。
而我們使用的C++、Java等語言並沒有提供這一語言特性。客戶程序的“期望”,設計繼承體系時,對方法調用的“契約”缺乏足夠的思考,正是產生設計違背LSP的一大原因。
(三)面向契約設計
派生類的前置條件和後置條件是:
p.108
Meyer規則: 在重新聲明派生類中的例程時,只能使用相等或者更弱的前置條件來替換原始的前置條件,只能使用相等或者更強的後置條件來替換原始的的後置條件。
一個方法,可以註明一個方法的前置條件和後置條件。可以在單元測試中指定契約。
(四)解決方案:
一種方法是,當繼承體系違背LSP時,提取公共部分,抽象成接口,來代替繼承。
這種情況還是很常見的,特別是當父類的方法,子類無法實現其功能,子類無法替換基類,而兩者確實有相同的部分,就抽取重構出來咯。
4. 依賴倒置原則(DIP)
(一)概念:
a. 底層模塊不應該依賴於底層模塊。兩者都應該依賴於抽象。
b. 抽象不應該依賴於細節。細節應該依賴於抽象。
依賴倒置原則是框架設計的核心原則之一。
DIP甚至是面向對象設計與面向過程設計的區別標準之一。
p.86
當高層模塊制定了應用策略,並且直接依賴底層配置細節時,就暴露出不靈活性;這種不靈活性是由於++依賴關係的方向++導致的。
解決方法:
使用Strategy模式,實現依賴倒置。
(二)如何倒置依賴
我之前單獨看DIP這個名詞,實在想不出依賴的倒置,依賴還能倒置?!
其實,“倒置依賴”應當與“依賴於抽象”、“層次化”結合起來理解。
這兩張圖前後一對比,就清晰了:
- 第一幅圖
高層是策略模塊,底層是細節實現模塊。這幅圖,上一層依賴於下一層,這樣的壞處就是策略模塊要隨着細節變化,也喪失了高層設置策略模塊的可複用性。這正是面向過程設計的依賴特點。
- 第二張圖
可以看出,如何實現依賴倒置的。正是依賴於抽象!底層實現細節依賴於高層策略模塊定義的接口。
通過將具體細節隱藏在倒置的接口之後,隔離了它們的不穩定性。
同樣,接口維繫起了系統各層的隔離性。
(三)具體的情景
a. 當我們應用了DIP,我們往往發現是客戶擁有抽象接口,而它們的服務者則從這些抽象接口派生。
b. 框架中,有時有必要創建具體類的實例,而創建這些實例的模塊將會依賴於它們。有種情況,字符串是具體的穩定的,所以依賴於它不會造成損害。所以,框架中將類的名字作爲配置數據傳遞給程序。
(四)解除依賴關係的兩種方式
動態多樣性
靜態多樣性
靜態多樣性,就是泛型、模板。能夠解決源代碼中的依賴關係,但不能解決動態多樣性所有的問題。
新類型會迫使泛型重新編譯、部署。
何時使用?
除非有嚴格的速度性能要求,否則應該優先使用動態多樣性。
5. 接口隔離原則
(一)概念:
不應該強迫客戶依賴於它們不用的方法。
提到接口隔離,就不得不說“胖類”的概念。胖類比如說,就是一個分析階段的產物,其中定義了很多方法的接口。
如果客戶程序直接依賴該接口,就得面臨着由於這些未使用方法的改變所帶來的改變。所有,ISP就是保證客戶程序僅僅依賴於它們實際調用的方法。
(二)解決方案:
使用多重繼承來分離接口。
適配器模式。
比如java GUI監聽的事件有很多,使用適配器模式,體現了ISP,將客戶程序只與特定的客戶程序接口綁定起來。
p.132
通過把胖類分解爲多個特定於客戶程序的接口,可以實現這個目標。每個特定於客戶程序的接口僅僅聲明它的特定客戶或者客戶組的那些函數。接着胖類就可以繼承所有於客戶程序的接口,並實現它們。這就解除了客戶程序和它們沒有調用的方法之間 的依賴關係,並使客戶程序之間不依賴。
通過兩張圖,我們就能輕鬆理解上述的ISP想表述的:
第一張
第二張
接口分離之後
其他語錄:
每個方法調用的開銷是1ns數量級。