我們知道對於很多數學問題,經常會有多種不同的解法
而且這其中可能會有一種比較通用簡便高效的方法
我們在遇到類似的問題或者同一性質的問題時,也往往采用這一種通用的解法
將話題轉移到程序設計中來
對於軟件開發人員, 在軟件開發過程中, 面臨的一般問題的解決方案就是設計模式(准確的說是OOP中)
當然,如同數學的解題思路一樣,設計模式並不是公式一樣的存在
設計模式(Design pattern)代表了最佳的實踐
是眾多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的寶貴經驗
是解決問題的思路
總之,設計模式是一種思想,思想,思想。
起源
隨着面向對象編程語言的發展,以及軟件開發規模的不斷擴大
編寫良好的OOP程序變得困難,而編寫可復用的OOP程序則更是困難
在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides
四人合著出版了一本名為 Design Patterns - Elements of Reusable Object-Oriented Software(中文譯名:設計模式 - 可復用面向對象軟件的基礎) 的書
該書首次提到了軟件開發中設計模式的概念。
四位作者合稱 GOF(四人幫,全拼 Gang of Four)
這就是設計模式四個字的起源
當然,即使在這本書出版之前,肯定也已經有很多有經驗的OOP程序員已經在使用自己的經驗(設計模式)了
但是這本書將OOP的設計經驗作為設計模式記錄下來
使我們能夠更加簡單方便的復用成功的設計經驗和體系結構
設計原則
"隨着面向對象編程語言的發展,以及軟件開發規模的不斷擴大
編寫良好的OOP程序變得困難,而編寫可復用的OOP程序則更是困難"
設計模式的起源, 正是需要設計模式的根本原因
借助於設計模式,可以更好地實現代碼的復用,增加可維護性
怎么才能更好地實現代碼復用呢?
面向對象有幾個原則:
根本原則
開閉原則(Open Closed Principle,OCP) 一個軟件實體應當對擴展開放,對修改關閉 。
即軟件實體應盡量在不修改原有代碼的情況下進行擴展
|
在開閉原則的定義中,軟件實體可以指一個軟件模塊、一個由多個類組成的局部結構或一個獨立的類
不修改已有代碼的基礎上擴展系統的功能的形式,就是符合開閉原則的
開閉原則的關鍵是抽象
比如,一個方法中
if(){
//...
}else if(){
//...
}
如果新增加一個邏輯功能點,則需要增加新的else 或者 else if ,勢必修改了已有代碼
而如果面向抽象的接口或者抽象類進行編程,擴展增加新的功能,只需要傳遞新的子類即可,原有的代碼功能不會有任何的修改
再比如
實際項目開發的時候,我們會把一些配置寫入到配置文件中,而不是"硬編碼"到代碼中
修改參數設置的時候,源代碼無需更改,這也是符合開閉原則
開閉原則作為根本原則,並不限定某種具體場景,只要是符合了這一含義,就是符合開閉原則
總之,開閉原則就是別因為新增功能擴展改(老)代碼
六大原則
開閉原則是根本綱領,它是面向對象設計的終極目標
除了根本原則另外還有六大原則 , 則可以看做是開閉原則的實現方法
- 單一職責原則 (Single Responsiblity Principle SRP)
- 里氏替換原則(Liskov Substitution Principle,LSP)
- 依賴倒轉原則(Dependency Inversion Principle,DIP)
- 接口隔離原則(Interface Segregation Principle,ISP)
- 合成/聚合復用原則(Composite/Aggregate Reuse Principle,C/ARP)
- 迪米特法則(Principle of Least Knowledge,PLK,也叫最小知識原則)
單一職責原則 (Single Responsiblity Principle SRP)
一個類只負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因
單一職責的原則很簡單,就是一個實體(一個類或者一個功能模塊)不要承擔過多的責任
承擔了過多的責任也就意味着多個功能的耦合
堆積木時, 到底是一塊積木比較容易利用, 還是多塊積木拼接起來的"一大塊" 更容易利用? 結果顯而易見
而且,承擔了過多的責任,也就是可能會因為多個原因修改這段代碼
隨之而來的是不穩定性以及維護成本的增加,也就是將會有多個原因引起他變化
單一職責原則的根本在於控制類的粒度大小
里氏替換原則(Liskov Substitution Principle,LSP)
里氏替換原則是以提出者 Barbara Liskov 的名字命名的
定義:
如果對每一個類型為 T1的對象 o1,都有類型為 T2 的對象o2
使得以 T1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時,程序 P 的行為沒有發生變化
那么類型 T2 是類型 T1 的子類型
簡單說就是 如果 一個程序P(T1) ,如果將輸入T1 替換為T2 ,而且 P(T1) = P(T2)
那么T2 是T1的子類型
再簡單的概述就是:
所有引用基類的地方必須能透明地使用其子類的對象
透明也就意味着不感知,不受任何影響
聽起來好像很自然的就可以做到
假如子類覆蓋了父類的方法呢?假如子類覆蓋了父類的方法並且改變了父類方法的原有功能邏輯呢?
比如,原來傳遞來兩個參數進行加法運算,子類覆蓋后,進行減法運算,會發生什么?
里氏代換原則的根本,在軟件中將一個基類對象替換成它的子類對象,程序將不會產生任何錯誤和異常
想要透明的使用子類,滿足里氏替換原則
需要注意應該盡可能的將父類設計為抽象類或者接口
讓子類繼承父類或實現父接口,並實現在父類中聲明的方法,這樣可以做到滿足開閉原則
子類的所有方法必須在父類中聲明,或子類必須實現父類中聲明的所有方法,也就是父類定義,子類實現
而且,子類不應該破壞父類的契約,也就是不能更改原有的方法的邏輯含義
里氏替換是繼承復用的基石,只有當子類可以替換父類,且軟件單位的功能不受到影響時
父類才能真正被復用,而子類也能夠在基類的基礎上增加新的行為
里氏代換原則是對開閉原則的補充。
實現開閉原則的關鍵步驟就是抽象化,而基類與子類的繼承關系就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規范
依賴倒轉原則(Dependency Inversion Principle, DIP)
抽象不應該依賴於細節,細節應當依賴於抽象; 換言之,要針對接口編程,而不是針對實現編程
也就是使用接口和抽象類進行變量類型聲明、參數類型聲明、方法返回類型聲明,以及數據類型的轉換等,而不是使用具體的類
在需要時,將具體類的對象通過依賴注入(DependencyInjection, DI)的方式注入到其他對象中
在引入抽象層后,程序中盡量使用抽象層進行編程, 系統將具有很好的靈活性 並且將具體類寫在配置文件中
如果系統行為發生變化,只需要對抽象層進行擴展,並修改配置文件
而無須修改原有系統的源代碼 , 擴展系統的功能無需修改原來的代碼,滿足開閉原則的要求
接口隔離原則(Interface Segregation Principle,ISP)
使用多個專門的接口,而不使用單一的總接口,即客戶端不應該依賴那些它不需要的接口
根據接口隔離原則,當一個接口太大時,我們需要將它分割成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法即可
接口隔離根本在於不要強迫客戶端程序依賴他們不需要使用的方法
合成/聚合復用原則(Composite/Aggregate Reuse Principle,C/ARP)
復用一個類有兩種常用形式,繼承和組合
盡量使用對象組合,而不是繼承來達到復用的目的,因為繼承子類可以覆蓋父類的方法,將細節暴露給子類
而且會建立強耦合關系,是一種靜態關系,不能再運行時更改等等弊端
個人建議,對於繼承的態度是不濫用,不棄用,帶着腦子用!
迪米特法則(Principle of Least Knowledge,PLK,也叫最小知識原則)
一個軟件實體應當盡可能少地與其他實體發生相互作用
也就是一個對象應當對其他對象有盡可能少的了解
再設計系統時,應該盡可能的減少對象之間的交互
有一個形象的說法"不要和“陌生人”說話、只與你的直接朋友通信"
下面這些一般被認為是朋友
"不要和“陌生人”說話、只與你的直接朋友通信" 就能夠最大程度的降低耦合性
類之間的耦合度越低,就越有利於復用
如果兩個對象之間不是必須要直接通信,那么這兩個對象就可以不發生任何直接的相互作用
而是可以通過第三者轉發這個調用,通過引入第三者將耦合度降低
設計原則總結
設計原則要求
設計原則是指導思想,將規則落實到具體的類/接口的設計、功能邏輯的划分上,可以轉化成以下要求
所有的要求都有一個前提:如果可以,應該優先考慮,盡可能的
- 面向抽象(抽象類、接口)編程,而不是面向實現編程
- 接口和類的功能要盡可能的單一,避免大而全的類和接口
- 優先使用組合,而不是繼承
- 子類的所有方法必須在父類中聲明,或子類必須實現父類中聲明的所有方法
- 子類應該盡可能的與父類保持一致,不要重寫父類原有邏輯
- 如果類之間沒必要直接交互,可以通過“中介”,而不是直接交互,降低耦合性
- 實現和細節可以通過DI的方式,最大程度減少“硬編碼”
- 如果沒有什么明顯弊端,類應該被設計成不變的
- 降低其他類對自身的訪問權限,不要暴露內部屬性成員,如果需要提供相應的訪問器(屬性)
設計模式與設計原則
設計原則是軟件開發過程中,前人以“高內聚,低耦合” “提高復用性”“提高可維護性”為根本目標
在實踐中總結出來的經驗,進而演化出來的具體的行為准則
就好似要做“好”一件事情,那么“好”的標准是什么?
按照經驗總結歸納出來的一些“好”的標准,就是程序設計中的設計原則
設計原則是站在不同的維度與角度思考問題的, 他們的根本目的是相同的
本質都是為了設計一個“易維護、可復用、高內聚低耦合”的程序
比如單一職責原則與接口隔離原則,本質都是要職責專一
類提供單一的功能的實現,接口不要有大而全的功能約定
職責專一就能降低耦合,就更有可能被復用
使用組合而不是繼承可以避免子類對父類的修改這種情況也就符合了里氏替換原則,也就符合了開閉原則
依賴倒置原則要求面向抽象進行編程而不是面向具體細節,而且依賴注入DI的思想也是如日中天Spring的根本
“易維護、可復用、高內聚低耦合”是目標
設計原則是為了達到目標的具體規則
而設計模式則是符合設計規則的具體的類/接口的設計解決方案
也就是設計原則的具體化形式
更准確的說,一個設計良好的程序應該遵循的是設計原則,而並非一定是某個設計模式
所有的原則都是指導方針,而不是硬性規則
是在很多場景下一種優秀的解決方案,而並不是一成不變的
在實際的項目中,你既不能完全放棄使用繼承,也可能讓一個類完全不同“陌生人”講話
也不可能子類完全不重寫父類的方法
面向抽象進行編程,你也不可能讓項目中所有的類都有抽象對應,這也是不可能的,也不能是被允許的
設計模式設計原則是經驗之談,當然是非常寶貴的經驗,也是經過實踐檢驗過的
但是最大的忌諱就是生搬硬套,矯枉過正,那將是最失敗的設計模式的應用方式
設計模式和面向對象的設計原則是解決問題的一般思路
而不是像交規一樣,必須遵守,嚴格執行
不遵守設計原則與設計模式也不會編譯失敗
但是希望能夠盡最大可能的遵守, 當然,還需要因地制宜而不能生搬硬套
或許,你從來不遵守原則,也不使用設計模式,你的代碼可能看起來仍舊好好地
但是
你的代碼出問題的概率
卻會比使用了設計模式遵循了設計原則的代碼
要大得多
設計模式和設計原則正是為了能夠更加簡單便利的復用代碼,盡可能的減少問題的出現
就好像一條淺淺的小河,可能有無數種趟過去方案
但是,那條走的人最多的,可能它並不是最好的
但是他肯定是比較合適的一條途徑,不會出現碎玻璃,沙坑等陷阱.
到底是站在巨人的肩膀上還是一定要自己摸着石頭過河?
簡單說來就是:我們知道軟件的目標“正確、健壯、靈活、可重用、高效....”等等,總之都是往“優秀”“好”的方向
然后發現了好的軟件的一些特性,所以作為了設計原則
但是還是過於抽象,於是針對於不同的場景,按照設計原則,整理出來一套好的解決方法,這就是設計模式。
設計模式分類
- 將系統所使用的具體類的信息封裝起來
- 隱藏了類的實例是如何被創建和組織的
行為型
設計模式之間並不是孤立的,他們也會相互使用,下圖為《設計模式 - 可復用面向對象軟件的基礎》一書中的描述
各個模式之間的區別和聯系是一個“悟”的過程,不要試圖對下下圖進行任何記憶
另外還有范圍准則的概念,指定模式主要是用於類還是用於對象
類模式處理類和子類之間的關系,這些關系通過繼承建立,是靜態的,編譯時刻便已經確定下來了
對象模式處理對象之間的關系,這些關系在運行時是可以變化的,更具有動態性
其實如果較真,很多的模式都有涉及到繼承/實現
所以說設計模式中常說的“類模式”只是指那些集中於處理類間關系的模式
大部分模式都屬於對象模式
比如對於創建型來說分為 類創建型模式和對象創建型模式
概念只是為了更好的描述問題,類模式和對象模式的概念也來自《設計模式 - 可復用面向對象軟件的基礎》
本人認為對於設計模式一般的學習與理解,這個概念無所謂
總結
設計模式是設計原則在解決具體問題時實踐中的運用
所以根本是要理解設計原則的含義
隨着技術發展,會出現更多的不同的問題場景,基於設計原則,可能拓展出來更多的設計模式
事實上到目前為止,也不僅僅是23種
所以說設計模式的根本是設計原則,而設計原則又是為了達到實現一個“優秀”軟件的行為准則。
在你還不能靈活的運用設計原則時,設計模式則是你的墊腳石,讓你在具體的問題面前能夠寫出更好地代碼
設計模式是理論層次的研究學習,自然是枯燥的
而且很難能夠一開始就高屋建瓴的自頂而下的深入理解
也很難徹底領悟設計原則本身
所以,從一個一個模式的學習中慢慢品味設計原則的精髓
類、接口之間的層級結構是可以變換的,設計模式的根本是設計原則
所以說在學習中要領悟設計模式的根本思想使用場景
在實踐中,不要生搬硬套的應用模式,也無需同設計模式中的類、接口設計層級結構一模一樣
可能你應用了某個模式,但是可能又根據實際業務有一些變動或調整
有人說,你這不是設計模式,那又如何?
只要能夠滿足需求符合設計原則,往“可復用/易維護/高內聚/低耦合”的目標前進,就好~
設計模式將“只可意會,不可言傳”轉變為“不只意會,還可以言傳~”