1 前言
看過許多關於設計模式的部落格,也讀過關於設計模式的書。幾乎所有的介紹的開頭,直接就引入了“設計模式”或者“某某模式”。設計模式到底是因什麼而來?這是一個很重要的問題。孫悟空從石頭縫裡蹦出來,《西遊記》還介紹了這個石頭的來歷呢。
要想了解一個東西,至少有“3W”——what、why、how——是什麼、為什麼、怎麼用。看現在大部分的文章或者書籍,重點介紹的還是“what”,這就有點類似於:為了用設計模式用設計模式。在這種思想的教導下去了解設計模式,學不會也很正常。
另外,介紹一個東西的用處時,不要弄一些小貓小狗、肯德基、打籃球、追MM這話總例子。這就像用小學課本的兒童故事來給你講解一個人生道理一樣,你聽著明白,但是真能理解嗎?
2 概述
記得之前看過一篇部落格,具體內容現在都忘記了。記得上面有句話大體是什麼說的:所謂的設計模式,我們日常工作中經常用,只是我們沒有想過像GoF一樣,把這些日常用到的模式總結歸納,形成結構化的理論。
可見,設計模式不真正是GoF提出的概念,而是他們作為一個有心人,把人們日常工作中遇到的設計問題,全面的總結,才形成了之後的“23種設計模式”。
首先,設計模式解決的肯定是系統設計問題,而且會用到物件導向來解決的。所以,本書開頭先說設計原則和麵向物件。物件導向基礎知識,大部分人應該都瞭解;至於設計原則,不瞭解的人必須要先了解。
其次,我們將模擬一個簡單的物件宣告週期過程,從物件的建立、封裝、組合、執行和操作,一步一步走來,會遇到許多情況和問題。針對問題,我們將通過思考,利用物件導向和設計原則,解決這個問題。而解決這個問題的方法,便是一種設計模式。
最後,23種設計模式不是一盤散沙,是有關係的。就是物件的生命週期一步一步的將各個設計模式串聯在了一起。物件的生命週期中,會一步一步的遇到總共23種設計問題,所以才會有23種設計模式。
3 設計原則
設計模式解決的肯定是系統設計的問題,所以首先從“設計”說起。
設計所要解決的主要問題,是如何高效率、高質量、低風險的應對各種各類變化,例如需求變更、軟體升級等。設計的方式主要是提取抽象、隔離變化,有5大設計原則——“SOLID”,具體體現了這個思路。
- S - 單一職責原則:
一個類只能有一個讓它變化的原因。即,將不同的功能隔離開來,不要都混合到一個類中。
- O - 開放封閉原則:
對擴充套件開放,對修改封閉。即,如果遇到需求變化,要通過新增新的類來實現,而不是修改現有的程式碼。這一點也符合單一職責原則。
- L - Liskov原則:
子類可以完全覆蓋父類。
- I - 介面隔離原則:
每個介面都實現單一的功能。新增新功能時,要增加一個新介面,而不是修改已有的介面,禁止出現“胖介面”。符合單一職責原則和開放封閉原則。
- D – 依賴倒置原則:
具體依賴於抽象,而非抽象依賴與具體。即,要把不同子類的相同功能抽象出來,依賴與這個抽象,而不是依賴於具體的子類。
總結這些設計原則可知,設計最終關注的還是“抽象”和“隔離”。物件導向的封裝、繼承和多型,還有每個設計模式,分析它們都離不開這兩個詞。
4 物件導向基礎
繼承、封裝、多型
介面、抽象類
5 一個物件的生命週期
一個物件在系統中的生命週期可以概括為以下幾點:
- 物件建立:
想到物件建立,最多的就是通過new一個型別來建立物件。但也會有許多特殊的情況,例如物件建立過程很複雜,如何解耦?等等。
- 物件組合、包裝:
一個物件建立後,可能需要對其就行包裝或者封裝,還可能由多個物件組成一個組合結構。在這過程中,也會遇到各種問題。
- 物件操作:
物件建立了,也組合、包裝完畢,然後就需要執行物件的各種操作,這是物件真正起作用的階段。物件的操作情況眾多,問題也很多。
- 物件消亡:
直到最後物件消亡,在C#中將被GC回收。
以上簡單介紹這個過程,其中的具體描述以及遇到的情況和問題,會在下文中詳細講解
6 建立一個物件
6.1 過程描述
一般物件的建立可以new一個型別,相信系統中絕大部分的物件建立也是這麼做的。但是如果遇到以下情況,直接用new一個型別,會遇到各種各樣的問題。
6.2 情況1:拷貝建立
系統中肯定會遇到這種情況,新建物件時,要用到一個現有物件的許多屬性、方法等。這時候再通過new一個新的空物件,還需要把這些屬性、方法都賦值到新物件中,帶來不必要的工作量。
提出這個問題,我們會想到克隆,也可能已經在系統中用到了克隆。其實這個就是一個比較簡單的設計模式——原型模式。我們把這個“克隆”動作抽象到一個介面中,需要克隆的型別,實現這個介面即可。
C#已經在FCL(Framework Class Library)中定義了一個介面——IColoneable,因此不需要我們在自己定義該介面,只需要在用到的地方實現即可。IColoneable介面只定義了一個Colone方法:
例如FCL中的String類,實現了IColoneable介面,並實現了介面方法Colone()。
6.3 情況2:限制單一物件
如果一個物件定義的屬性和方法,可供系統的所有模組使用,例如系統的一些配置項。此時無需再去建立多個物件。也不允許使用者建立多個物件,因為一旦修改,只修改這一個物件,系統的所有模組都將生效。
我們把這個只能例項化一次的物件叫做“單例”,這種模式叫做單例模式。
其實系統中的靜態類,就是這種“單例”的設計思想。例如FCL中的Console類,它是一個靜態類,它給系統提供的就是一個“單例”類。
只不過Console是一個型別,而不是物件,缺點就是無法作為物件賦值和傳遞。如果系統中需要的“單例”就是一些功能,涉及不到物件的賦值和傳遞,完全可以用靜態類實現,沒必要非得用單例物件。
物件的單例模式,關鍵在於限制型別的建構函式,不讓使用者隨意new一個新物件,且看程式碼:
重點:將建構函式設定為private,只能內部呼叫;用一個靜態欄位來儲存物件。
可見,無論單例是型別還是物件,都需要通過“靜態”來實現。
6.4 情況3:複雜物件
建立一個新物件時,一般需要初始化物件的一些屬性。簡單的初始化可以用通過建構函式和直接賦值來完成。
但是如果一個物件的屬性過多,業務邏輯很複雜,就會導致複雜的建立過程。這種情況下,用建構函式是不好解決的。如果用直接賦值,就會導致大量的if…else…或者switch…case...的條件判斷。這樣的程式碼將給系統的維護和擴充套件帶來不便,而且如果不改變設計,會隨著維護和擴充套件,會出現更多的條件判斷。隨著程式碼量的增加,維護難度更大。如果再是多人同時維護,那就麻煩了。
顯然,這樣的程式碼不是我們所期望的。設計上也不符合單一指責原則、開放封閉原則。所以,對於一個複雜物件的建立過程,我們將考慮重構。
我們把物件建立的過程抽象出來,做成一個框架,然後派生不同的子類,來實現不同的配置。將複雜物件的構建與其表示分離,這就是建造者模式。
上圖中,我們最終要建立的是Product型別的物件,Product是個複雜物件。如果直接new一個物件,再賦值,會導致大量條件判斷。
所以,我們將物件建立過程抽象到一個Builder抽象類中,然後用不同的子類去實現具體的物件建立。這樣的設計相比之前大量的if-else-程式碼,優勢是非常明顯的,並且符合單一職責原則和開放封閉原則。應對需求變更、新功能增加、多人協同開發都是有好處的。
6.5 情況4:功能相同的物件
最經典的就是資料操作。建立一個用於SQL server的SQLDBHelper類,又建立了一個用於Oracle的OracleDBHelper類,這兩個類所實現的功能是完全一樣的,都是增刪改查等。如果這兩個類是孤立的,那系統資料庫切換時候,將導致SQLDBHelper和OracleDBHelper兩個類之間的切換,而且改動工作量隨著系統複雜度增加。
而且如果增加一個資料庫型別,也會導致系統程式碼的大量修改。
這個問題的根本是違反了依賴倒置原則。客戶端應該依賴於抽象,而不是具體實現。我們應該把資料操作的功能抽象出來,然後通過派生子類來實現具體。
這樣設定之後,我們建立物件的程式碼就會變成:
面對不同的資料庫,我們需要判斷並建立不同的實現類。
可以把這段程式碼封裝成一個方法,這就是一個簡單的“工廠”。所謂工廠,就是封裝一個物件建立過程,對於一種抽象,到底由哪個具體實現,由工廠決定。
這是一個簡單工廠模式。另外,工廠方法模式、抽象工廠模式也都是在這個基礎上再去抽象、分離,而出來的。
6.6 總結
物件建立並不是new一個型別這麼簡單,以上四種情況在日常開發過程中應用也都比較常見。
上面通過物件建立過程的種種情況,隨之介紹出了:原型模式、代理模式、建造者模式、工廠模式。雖然現在還不能完全瞭解這些模式的細節,但是至少明白了這些模式應對什麼問題,有了明確的定位。而這才是最關鍵的,有了定位,有了高層次的理解,再看細節就變得容易多了。
後文繼續,敬請期待!
---------------------------------------------------------------------------------------------
7. 多物件組成結構
7.1 過程描述
7.2 情況1:借用外部介面
7.3 情況2:給物件增加新功能
7.4 情況3:封裝功能
7.5 情況4:遞迴關係的組合
7.6 情況5:分離多層繼承
7.7 情況6:封裝組合,供客戶使用
7.8 總結
8. 物件行為與操作物件
……
下一篇:換種思路去理解設計模式(中)