[設計模式]之六大設計原則
就一個類而言,應該僅有一個引起它變化的原因。
假設現在要在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
在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,并且需要原有代碼經過重新測試。所以當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。
但在設計軟件的時候,無論模塊是多么的封閉,都會存在無法對之封閉的變化,因為你不可能在編碼前就考慮到所有情況。所以在設計代碼時就必須先猜測出最可能發生變化的種類,然后構造抽象來隔離變化。在編碼之后,一旦遇到發生變化的地方,那就應該首先考慮要不要對這里進行結構的修改。也就是 遇到變化發生時要立即采取行動 。
比如現在在客戶端類中寫了一個加法程序,后來說要增加減法,那么這時就應該立即抽象出來一個運算類。雖然說直接在客戶端增加減法算法很快,但考慮到以后也許會拓展更多的算法,而且代碼改得越晚修改代碼的范圍就越大。立即修改代碼結構的代價似乎比以后去改的代價要小很多。
我們希望的是在開發工作展開不久就知道可能發生的變化。查明可能發生的變化所等待的時間越長,要創建正確的抽象就越困難開放-封閉原則是面向對象設計的核心所在。遵循這個原則可以帶來面向對象技術所聲稱的巨大好處,也就是可維護,可拓展,可復用,靈活性好。開發人員應該對程序中呈現出頻繁變化的那些部分作出抽象,然而,對于應用程序中的每個部分都刻意地進行抽象同樣不是一個好主意。拒絕不成熟的抽象和抽象本身一樣重要。
參考
http://blog.csdn.net/zhengzhb/article/category/926691/
via: http://www.wossoneri.com/2016/05/16/[Design-Pattern]Principles/