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. 對象行為與操作對象
……
下一篇:換種思路去理解設計模式(中)