"設計模式系列目錄" 新博客 "wossoneri.com" 單一職責原則 Single Responsibility Principle SRP 就一個類而言,應該僅有一個引起它變化的原因。 假設現在要在iPhone上做一個圖片編輯工具。功能有裁剪圖片,旋轉圖片,縮放移動照片等等。 吶,我們可以寫 ...
單一職責原則 Single Responsibility Principle - SRP
就一個類而言,應該僅有一個引起它變化的原因。
假設現在要在iPhone上做一個圖片編輯工具。功能有裁剪圖片,旋轉圖片,縮放移動照片等等。
吶,我們可以寫一個功能集類,然後把這些所有操作視為功能集的一部分,把代碼全部寫進這個類裡面。
這麼看來似乎可以,因為這是作為一個單獨的模塊嘛,把相關功能寫進一個工具類里,用哪個功能調用哪個函數就好了。但這帶來了一個問題就是這個工具類包含過多功能顯得非常臃腫,不容易維護。而且在一個類里往往容易出現幾個函數共用一個全局變數的情況,功能之間耦合度太大,難以復用。
舉個最直接的例子:如果我想把這個功能移植到Android上去怎麼辦。這個移植過程麻煩之處並不在於語言語法變化,而是兩個系統有著完全不同的手勢傳遞機制,我要用手旋轉,縮放圖片這段代碼完全沒法復用,唯一能用的裁剪代碼,也可能因為和其他代碼耦合過大導致需要重新修改,退一步說,裁剪演算法就算沒有耦合,代碼可以直接用,但關係到手勢的代碼對我來說都成為冗餘代碼,這對於代碼復用就是災難。
如果一個類承擔的職責過多,就等於把這些職責偶合在一起,一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。這種耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞。
所以這裡在設計的時候,就要考慮一下把這些功能分類。比如裁剪功能需要知道裁剪框大小,位置。那就分離出一個類,專門負責計算裁剪框四個點的坐標變化。旋轉縮放圖片需要知道圖片的大小,縮放率,顯示方向等信息,那就再分離出一個類,負責計算圖片形態的變化。最後剩下手勢再封裝一個類,處理手勢的邏輯,在不同情況下獲取不同的手勢數據,作為參數交給上面兩個演算法類進行計算輸出。
這樣一來,每個類的職責就變得單一了,維護就容易多了。後面再移植代碼的話,演算法類只需要切換語法,手勢類只要去重寫觸發手勢的條件,而不必修改邏輯。代碼很快就可以改好,並且不會破壞原有的項目結構。
軟體設計真正要做的許多內容,就是發現職責並把那些職責相互分離。判斷是否要分離出類的方法就是,如果你能夠想到多於一個的動機去改變一個類,那麼這個類就具有多於一個的職責,就應該考慮類的職責分離。
優點
- 可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多
- 提高類的可讀性,提高系統的可維護性
- 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響
里氏替換原則 Liskov Substitution Principle - LSP
子類型必須能夠替換掉他們的父類型
通俗的講,一個軟體實體如果使用的是一個父類的話,那麼一定適用於其子類,而且它察覺不出父類對象和子類對象的區別。即,在軟體里,把父類都替換成它的子類,程式的行為沒有變化。
里氏替換原則的重點在不影響原功能,而不是不覆蓋原方法。
所以正常遵從該原則的處理辦法是在需要覆蓋父類方法時應該首先考慮使用super調用父類的同名方法以保證父類同名方法會被調用。
如果確實不需要調用父類方法,則不加此語句。
這個原則很重要,編碼時要註意。
依賴倒置原則 Dependence Inversion Principle - DIP
抽象不應該依賴細節,細節應該依賴抽象
通俗的說,就是要針對介面編程,不要對實現編程。吶,比如說電腦主板,CPU,記憶體,硬碟這些硬體的設計就是依賴介面設計的。單拿CPU來說,CPU有各種廠家設計的各種型號,這些型號的內部設計實現都不相同,但他們的介面是一樣的,這樣主板就可以隨意更換CPU了。
關於倒置,比如說我有一個高層模塊,模塊實現對SQLite讀寫的功能依賴一個控制訪問SQLite的低層模塊。一旦我要求把SQLite改為MySQL,那這個低層模塊就無法正常工作,進而倒置上層模塊也無法正常工作。依賴倒置就是說設計代碼不再是上層依賴下層,而是兩層都去依賴介面去實現,這樣兩層的運行狀態便不會互相影響。
依賴倒轉其實可以說是面向對象設計的標誌,用哪種語言來編寫程式不重要,如果編寫時考慮的都是如何針對抽象編程而不是針對細節編程,即程式中所有的依賴關係都是終止於抽象類或者介面,那就是面向對象的設計,反之那就是過程化的設計了。
依賴倒置原則的實現可以參考策略模式:設計模式之二:策略模式
例子中的收取現金的不同方式可以看做CPU的不同型號。調用收現金的方法可看做主板插上不同型號的CPU。就是這麼個思想。
遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程式造成的風險。
根據該原則,編程中要註意
- 低層模塊儘量都要有抽象類或介面,或者兩者都有
- 變數的聲明類型儘量是抽象類或介面
- 使用繼承時遵循里氏替換原則
介面隔離原則 Interface Segregation Principle - ISP
客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上
看圖,圖一是未遵循該原則的結構:
介面隔離原則的含義是:建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。也就是說,我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去調用。
介面是設計時對外部設定的“契約”,通過分散定義多個介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。
註意事項
- 介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性是不掙的事實,但是如果過小,則會造成介面數量過多,使設計複雜化。所以一定要適度
- 為依賴介面的類定製服務,只暴露給調用的類它需要的方法,它不需要的方法則隱藏起來。只有專註地為一個模塊提供定製服務,才能建立最小的依賴關係
- 提高內聚,減少對外交互。使介面用最少的方法去完成最多的事情
迪米特法則 Law Of Demeter - LOD
一個對象應該對其他對象保持最少的瞭解。
如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中一個類需要調用另一個類的某一個方法,可以通過第三者轉發這個調用。
迪米特法則首先強調的前提是在類的結構設計上,每一個類應當儘量降低成員的訪問許可權,也就是要降低類之間的耦合。類之間的耦合越弱,越有利於復用,修改類相互之間的影響也會降到最低。
迪米特法則還有一個更簡單的定義:只與直接的朋友通信。首先來解釋一下什麼是直接的朋友:每個對象都會與其他對象有耦合關係,只要兩個對象之間有耦合關係,我們就說這兩個對象之間是朋友關係。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變數、方法參數、方法返回值中的類為直接的朋友,而出現在局部變數中的類則不是直接的朋友。也就是說,陌生的類最好不要作為局部變數的形式出現在類的內部。
迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關係。但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發生聯繫,例如本例中,總公司就是通過分公司這個“中介”來與分公司的員工發生聯繫的。過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統複雜度變大。所以在採用迪米特法則時要反覆權衡,既做到結構清晰,又要高內聚低耦合。
開閉原則 Open Close Principle - OCP
軟體實體(類,模塊,函數等)應該可以拓展,但是不可修改
這個原則有兩點:
- 對於拓展是開放的 Open for extension
- 對於更改是封閉的 Closed for modification
在軟體的生命周期內,因為變化、升級和維護等原因需要對軟體原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。所以當軟體需要變化時,儘量通過擴展軟體實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。
但在設計軟體的時候,無論模塊是多麼的封閉,都會存在無法對之封閉的變化,因為你不可能在編碼前就考慮到所有情況。所以在設計代碼時就必須先猜測出最可能發生變化的種類,然後構造抽象來隔離變化。在編碼之後,一旦遇到發生變化的地方,那就應該首先考慮要不要對這裡進行結構的修改。也就是遇到變化發生時要立即採取行動。
比如現在在客戶端類中寫了一個加法程式,後來說要增加減法,那麼這時就應該立即抽象出來一個運算類。雖然說直接在客戶端增加減法演算法很快,但考慮到以後也許會拓展更多的演算法,而且代碼改得越晚修改代碼的範圍就越大。立即修改代碼結構的代價似乎比以後去改的代價要小很多。
我們希望的是在開發工作展開不久就知道可能發生的變化。查明可能發生的變化所等待的時間越長,要創建正確的抽象就越困難
開放-封閉原則是面向對象設計的核心所在。遵循這個原則可以帶來面向對象技術所聲稱的巨大好處,也就是可維護,可拓展,可復用,靈活性好。開發人員應該對程式中呈現出頻繁變化的那些部分作出抽象,然而,對於應用程式中的每個部分都刻意地進行抽象同樣不是一個好主意。拒絕不成熟的抽象和抽象本身一樣重要。