换种思路去理解设计模式
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:借用外部接口
有開發經驗的人知道,日常大部分開發都是在已有系統基礎上開發的。即便是從新開發的系統,也要依賴于一個框架或者庫。
所以,我們每時每刻都在用系統已有的接口。但是如果這些接口不滿足我們的需求,我們就需要重新對接口封裝一下,讓其符合當前的規則。就是這個我們日常用的技巧,被GoF總結成為一個模式——適配器模式。
不用看代碼和類圖,也能明白它的意思。不必太計較代碼和類圖的細節,重點在于理解設計思想。
顧名思義,適配器就是做一個隔離,起到了解耦的作用。例如我們日常用的筆記本電腦適配器。
7.3 ? 情況2:給對象增加新功能
系統總是在不斷的維護和升級當中,也可能在不斷的需求變更當中,因此為對象增加新功能,是再常見不過的了。那么如何為對象增加新功能呢?
最直接的回答就是改代碼唄。改類型的代碼,增加方法、屬性等。
對于這種修改,首先想到的應該是違反了“開放封閉原則”和“單一職責原則”,違反這種原則帶來的壞處很多。代碼越改越多;每次更改都有可能影響以前代碼;多人維護一個文件,不利于協同開發……
如果用“抽象”“隔離”的思想來思考這一問題,很容易就能找出思路:第一,把原有功能和新增功能隔離;第二,兩者都依賴于一個抽象,這個抽象就是對象應該有的所有功能;第三,外部客戶將依賴于抽象,它不會察覺內部的變化(依賴倒置原則)。
這就是裝飾模式。
從上面的類圖看,ConreteComponent是原始類型,Decorator是一個抽象類,它的派生類負責添加新功能。這里理解的重點,在于Decorator類中有一個Component屬性,相當于Decorator封裝了一個Component,直接調用他原有的功能,并且可以新增功能。當然,這些操作都是可以派生在子類中實現的。而且不同的子類可以實現增加不同的功能。
這樣的抽象和分離就符合開放封閉原則和單一職責原則,也不會出現代碼量過多、多人維護不便等問題。
7.4 ? ?情況3:封裝功能
對于有些功能,我們不希望客戶端直接調用,而是在調用時候先做一個判斷,或者加一個緩存。其實就是在真實功能和客戶端之間,加一個中間層。而這不能讓客戶端調用察覺。
如果你把這個中間層直接加入到真是功能中,雖然這可以不讓客戶端察覺,那將會給系統帶來隱患,違反“單一職責原則”。如下:
?
首先,如何不讓客戶端察覺?答案很簡單——依賴倒置原則——讓客戶端依賴于一個抽象。這個抽象將如何實現呢? 具體的實現和中間層都要去實現。如下:
?類圖如下:
?
這就是代理模式。
每個設計模式要體現的都是一種設計的思路,代理模式就是要在客戶端和底層實現加一層,在該層中實現一些業務場景。4s店就是客戶于汽車廠家的代理。
具體是否要都去實現同一個接口,這種細節不重要,不要去過于糾結這些類圖和代碼。
7.5 ? ?情況4:遞歸關系的組合
上文提到如何更有效率的維護對象功能和新增功能,以及更有效率的封裝對象。這兩種做法的輸出,其實還是一個單個的對象。如何將一個個對象組合成一個視圖,系統中最常見的無非是兩種——列表、樹,以及兩者的結合體——TreeGrid
列表是比較簡單的結構,按實際的需求應用,不會產生太多誤解。而樹結構卻有值得討論之處。最簡單的樹節點實現的代碼如下:代碼很簡單,只有節點的名稱,和對代碼下級節點的管理。
如果我們應對的業務很簡單,例如類似于windows系統的文件夾樹,即每個節點的類型都一樣,每個節點的功能也都一樣,葉子節點和摘要節點在功能上沒有區別。這種情況下,可以用以上代碼輕松應對。
但是如果遇到以下情況呢,如下圖:
這也是個樹結構,但是每個節點類型都不一樣,形式的功能也不一樣,“個人”是個葉子節點,不能再添加下級節點。這種情況下,再用以上那段代碼就會出現許多問題,如多個功能集中在一個代碼文件中,多人維護一段代碼等。
?如何解決這一問題,組合模式給予我們靈感。
根據以上類圖,可以看出組合模式解決這一問題的思路是:將樹結構中的節點的統一功能抽象出來,不同類型的節點,用不同的子類去實現。類圖中只有兩個子類,我們可以根據自己的實際情況來派生多個子類。
這樣解釋想必大部分人都能理解該模式的設計思路,不必再用代碼挨著表達了。關鍵在于理解如何分析問題,如何抽象,如何隔離,如何解耦,最終就是如何設計。
這樣設計符合開放封閉原則、職責單一原則,對于客戶端也符合依賴倒置原則。
7.6 ? 情況5:分離多層繼承
在對象組合過程中,難免會出現繼承的情況,甚至會出現多層繼承。根據設計原則——少繼承、多聚合。因此不建議我們使用多層繼承。而是盡量把這種多層繼承的關系,變成聚合的關系。
在一個多層繼承結構中,如果底層節點可以抽象出相同的功能,即可變為聚合關系。如:
如上圖,子類可以提取出一個抽象。變成這樣的設計:
這樣就把多繼承變成了聚合。
????????
可以總結歸納以下這種情況。我們把左側“發送消息”及其子類叫做“抽象”,右側的“發送方式”及其子類叫做“實現”。那么我們現在做的就是從“實現”和“抽象”的繼承關系,變成了兩者的聚合關系。
這就是——橋接模式。以下是類圖:
他應對的場景是抽象有多樣性,實現也有多樣性。抽象的抽象只依賴于實現的抽象。從而解耦抽象和實現的關聯。
7.7 ? 情況6:封裝組合,供外部使用
當一個組合封裝完成后,要提供統一的接口供外部客戶端使用。而不是讓客戶端在組合內部任意的調用。
這就是外觀模式。很好理解,也經常用到,可能只是不知道這個名字而已。它像一個包袱一樣包起來組合,只留規定的接口。
外觀模式簡單易懂,不需要類圖和代碼過多解釋。
7.8 ? 總結:
(注:未包括“Flyweight享元模式”。將在后續版本更新中加入。)
其實以上這幾種情況,就是結構性的設計模式對應的問題,每種情況對應一種設計模式。結合自己或多或少的開發經驗,仔細考慮分析這幾種情況,肯定每種情況都是你在編碼中遇到的,也是一個對象組合很可能需要的。
遇到這些問題時,你當時是怎么解決的?不一定非得按照設計模式上的解決方式。但是要已定符合設計原則。設計模式只是一個“術”,提供一個解決思路或者靈感,而設計原則、設計思想才是“道”。
?
特別說明:
上、中兩篇其實是我已經整理好的資料,直接復制粘貼到這里的(從word中往這里面貼圖挺麻煩),因此更新比較快。這些資料是我從5.12開始,每天都花一小時時間整理的,算來也兩周了。下一篇資料現在剛剛開始整理,估計不會很快就更新上。因為我需要把剩下的設計模式挨個分析,然后串起來,不容易。有需要的朋友得耐心等兩天,但是我肯定會在發出來。
開寫之前,先把前兩部分的鏈接貼上。要看此篇,不許按順序看完前兩部分,因為這本來就是一篇文章,只不過內容較多,分開寫的。
換種思路去理解設計模式(上)
換種思路去理解設計模式(中)
?
8?????? 對象行為與操作對象
8.1???? 過程描述
所謂對象行為和操作對象,需要三方面內容:
l??操作過程:
一般表現為一個方法。該方法接收一個對象或者組合類型的參數,然后對這個對象或者組合進行操作,例如修改屬性、狀態或者結構等。
l??操作的對象或組合:
會作為實參傳入操作過程,會被操作過程修改屬性、狀態或者結構等。
l??受影響的對象或組合:
由于修改其他對象或者組合,可能會影響到另外的對象或者組合,因此需要考慮這種影響關系,一般會用通知或者消息訂閱的方式實現。
?
從上文的對象創建和對象組合兩個模塊,應該理解出在系統設計中會遇到各種復雜的情況。而對象操作比前兩者更加復雜,因為前兩者是創建一個靜態的結構,而對象操作則是一個動態的變化。在日常的開發工作中,對象操作也是我們付出精力最多的地方。
下面我們就對象操作過程中遇到的一些常見情況做詳細的分析。
8.2???? 情況1:“多配置”操作的優化
當我們的一個方法因為需要實現多配置而不得不寫許多條件判斷語句時,我們會考慮將這個方法抽象出來,然后派生不同的子類。這是基本的設計思路。
現在我們將這個情況復雜化,業務場景多了,一個方法無法實現這些功能,就需要拆分。
如果這種情況下,再出現因為很多配置而不得不寫許多條件判斷語句時,我們肯定還需要再次考慮抽象和派生。效果如下圖:
?這就是——模板方法模式。
理解這個模式其實很簡單,只要知道根據多配置需要抽象、拆分即可。至于這里的“模板”,可根據實際情況來使用或者改變。
?
8.3???? 情況2:串行操作的優化
針對對象進行操作時,類似于流程一樣的串行操作,在系統中應用非常廣泛。而且各個串行的節點都有相對統一的操作過程,例如工作流的每個審批節點,都會修改對象狀態以及設置下級審批人等。
遇到這種場景,我們最初會思考以下思路:
后來隨著系統的升級和變更,代碼越來越多,維護越來越困難。我們會先考慮將每一步操作都獨立成一個方法:
?一般的串行操作,可以用以上代碼結構來處理,需要修改處可以再根據實際情況再重構。但如果串行操作中有條件因素,可能就有優化的空間了。如下代碼:
當隨著我們的條件越來越多,業務關系越來越負責時,維護這段代碼就越來越復雜,也可能因為多人維護而帶來版本問題。需要優化。
分析任何問題,都先要從業務上抽象。以上代碼抽象出來有兩點:“操作”和“條件”。“操作”很容易抽象的,但是“條件”卻不好抽象。沒關系,職責鏈模式給了我們靈感,我們可以把“條件”看作“下一步的操作”。
好了,我們把“操作”和“下一步的操作”抽象出來。然后將每一步的操作處理作為抽象的一個派生類。
如上圖,每個子類是一步處理。每一步處理都實現了抽象類的RequestHandler()方法,都繼承了抽象類的success屬性(即下一步執行者)。這樣我們就可以設計ConcreteHandler1的success為ConcreteHandler2,ConcreteHandler2的success為ConcreteHandler3,從而構成了一個“職責鏈”。
這就是職責鏈模式的設計思想。
?
8.4???? 情況3:遍歷對象各元素
當對一個對象操作時,遍歷對象元素是最常見的操作之一,使用Java和C#的人用的最多的就是for和foreach(先放下foreach不說,后文會有解釋),C和C++一般用For循環。For循環簡單易用,但是有它設計上的缺點,先看一段for循環代碼:
代碼中obj是個特別刺眼的變量,如果代碼較多了,就會出現滿篇的obj。這里的代碼是客戶端,過多的obj就會導致了大量的耦合。如果obj的類型一點有修改,就會可能導致整個代碼都要修改。
設計是為了抽象和分離。我們需要一種設計來封裝這里的obj以及對obj的遍歷過程。我們定義一個類型,它接收obj,負責遍歷obj,并把遍歷的接口開放給客戶端。
代碼中,我們通過Next()和IsDone()就可以完成一個遍歷。可以給客戶端提供First()、Current()、Last()等快捷接口。
這樣,我們可以用這種方式迭代對象。
代碼中只用到一個obj,因為我們如果再有迭代過程,可以用iterator對象,而不是obj。這就是迭代器模式的設計思路。
前文中提到了foreach循環。其實foreach是C#和java已經封裝好的一個迭代器,它的實現原理就是上文中講到的方法。在日常應用中,foreach在大部分情況下能滿足我們的需求。但是要真正理解foreach的用以,還需這個迭代器模式的幫助。
?
8.5 ? ? 情況4:對象狀態變化
改變一個對象的狀態,是再常見不過的操作了。例如一個對象的狀態變化是:
這幾乎是最簡單的流程了,我們一般的開發思路如下:
這是最基本的思路,如果再改進,可能會把狀態統一維護成一個枚舉,而不是硬編碼在代碼中直接寫“編輯”“結束”等。
但是這樣改進,代碼的邏輯和結構是不變的。仍然存在一個問題——當狀態變化的邏輯變復雜時,這段代碼將變得非常復雜——大家應該都明白,在真正的系統中,狀態變化可比上面那個圖片復雜N倍。
大家可能都注意到了,一旦遇到這種問題,那肯定是違反了開放封閉原則和單一職責原則。要改變這個問題,我們就得重構這段代碼,從設計上徹底避免。
首先要做的還是抽象,然后再去隔離和解耦。當前這個業務場景中,能抽象出來的是“狀態”和“狀態變化”。
那么我們就把狀態作為一個抽象類,把狀態變化這個都做作為抽象類中的抽象方法,然后針對每個狀態,都實現成一個子類。結構如下:
然后再把對象關聯到狀態上,根據依賴倒置原則,對象將依賴于抽象類而非子類。
上圖中,Context的狀態是一個State類型,所以無論State派生出多少個子類,都能成為Context的狀態。至于Context的狀態變化,就在State子類的Handle方法中實現。例如ConcreateStateA的handle方法,可以將Context的狀態賦值成ConcreteStateB對象,ConcreteStateB的handle方法,可以將Context的狀態賦值成ConcreteStateC(圖中沒有)對象……一次類推。這樣就將一個復雜的狀態變化鏈,分解到每一步狀態對象中。
這種設計思路就是——狀態模式。
8.6 ? ? 情況5:記錄變化,撤銷操作
上文提到如何改變對象的狀態,從這里我們可以想到狀態的撤銷,以及其他各個屬性修改之后的撤銷操作。撤銷操作的主要問題就在于如何去保存、恢復舊數據。
最簡單的思路是直接定義一個額外的對象來存儲舊數據,
如果需要撤銷,再從存儲舊數據的對象中獲取信息,重新賦值給主對象。
由此可能發現,上圖中客戶端的代碼非常繁瑣,而且客戶端幾乎查看到了主對象類型和封裝對象類型的所有信息,沒有了所謂的“封裝”。這樣帶來的后果是,如果主對象屬性有變化,客戶端立刻就不可用,必須修改。
其實客戶端應該只關注“備忘”和“撤銷”這兩件事情、這兩個操作,不必去關心主對象中有什么屬性,以及備忘對象有什么屬性。再思考,“備忘”和“撤銷”這兩個動作都是針對主對象進行的,主對象是這兩個動作的執行者和受益者。那么為何不把這兩個動作直接交給主對象進行呢?
根據以上思路重構代碼可得:
在原來代碼的基礎上,我們又給主對象增加了兩個方法——創建備忘和撤銷。接下來客戶端的代碼就簡單了。
正如我們上面所說的,客戶端關注的只是這兩個動作,而不關心具體的屬性和內容。
這就是——備忘錄模式。看起來很簡單,也很好理解。它沒有用到面向對象的太多特點,也沒有很復雜的代碼,僅僅體現了一個設計原則——單一職責原則。利用簡單的代碼對代碼職責就行分離,從而解耦。
8.7 ? ?情況6:對象之間的通訊 – 一對多
一個對象的屬性反生變化,或者一個對象的某方法被調用時,肯定會帶來一些影響。所謂的“影響”具體到系統中來看,無非就是導致其他對象的屬性發生變化或者事件被觸發。
近來在生活中遇到這樣的兩個場景。第一,白天用手機上的某客戶端軟件看NBA文字直播,發現只要某個球員有進球或者籃板之類的數據,系統中所有的地方都會更新這個數據,包括兩隊的總分比分。第二,看jquery源碼解讀時,jquery的callbacks的應用也是這種情況,事例代碼之后貼出。這兩種情況都是一對多通訊的情況。
(看以上代碼的形式,很像C#中的委托)
?先不管上面的代碼或者例子。先想想這種一對多的通訊,該如何去設計,然后慢慢重構升級。最簡單的當然是在客戶端直接寫出來,淺顯易懂。這樣寫代碼的人肯定大有人在(因為我之前曾是這樣寫的):
????????
如果系統中這段代碼只用一次,這樣寫是沒有問題的。但是如果系統有多地方都是context.Update(),那將導致一旦有修改,每個地方都得修改。耦合太多,不符合開放封閉原則。
解決這個問題很簡單,我們把受影響對象的更新,全部放到主對象的更新中。
再想想還會遇到一個問題:難道我們每次調用Context的Update時,受影響的對象都是固定的嗎?有工作經驗的人肯定回答否。所以,我們這樣盲目的把受影響對象的更新全部塞到Context的Update是有問題的。
其實我們應該抽象出來的是“更新”這個動作,而應該把具體的哪些對象受影響交給客戶端。如下:
上圖中,我們把受影響對象的類型抽象出一個Subject類,它有一個Update()方法。在主對象類型中,將保存一個Subject類型的列表,將存儲受影響的對象,更新時,循環這個列表,調用Update方法。可見,Context依賴的是一個抽象類——依賴倒置原則。
這樣,我們客戶端的代碼就成了這樣:
這次終于實現了我們的預想。可以對比一下我一開始貼出來的js代碼。效果差不多。
這就是大家耳熟但并不一定能詳的——觀察者模式。最常見的例子除了jquery的callbacks之外,還有.net中的委托和事件。此處不再深入介紹,有機會再把委托和事件如何應用觀察者模式的思路介紹一下。
8.8 ? ? 情況7:對象之間的通訊 – 多對多
上文中提到一對多的通訊情況,比一對多更復雜的是多對多的通訊。如果用最原始的模式,那將出現這種情況,并且這種情況會隨著對象的增加而變得更加復雜。N個對象就會有N*(N-1)個通訊方式。
所以,當你確定系統中遇到這種問題,并且越發不可收拾的時候,就需要重構代碼優化設計。思路還是一樣的——抽象、隔離、解耦。當前場景的復雜之處是過多的“通訊”鏈條。我們需要把所有的“通訊”鏈條都抽象出來,并隔離通訊雙方直接聯系。所以,我們希望的結構是這樣的。
其實這就是中介者模式。
以上只是一個思路,看它是怎么實現的。
?
首先,把所有需要通訊的類型(成為“同事”類型),抽象出一個基類,基類中包含一個中介者Mediator對象,這樣所有的子類中都會有一個Mediator對象。子類有Send()方法,用于發送請求;Notify()方法用于接收請求。
其次,中介者Mediator類型,基類中定義Send()抽象方法,子類中要重寫。子類中定義了所有需要通訊的對象,然后重寫Send()方法時,根據不同情況,調用不同的同事類型的Notify()方法。如下:
這樣,在同事類型中,每個同事類的Send()方法,就可以直接調用中介者Mediator的send()方法。如下:
最后,總體的設計結構如下:
越看似簡單的東西,就越難用。因為簡單的東西具有通用性,而通用就必須適應各種環境,環境不同,應用不同。中介者模式就是這樣一種情況。如果不信,可以具體思考一下,你的系統中哪個場景可以用中介者模式,并且不影響其他功能和設計。
在具體應用中,還是把重點放在這個設計的思路上,不必太拘泥與它的代碼和類圖,這只是一個demo而已。
8.9 ? ? 情況8:如何調用一個操作?
對于這個問題,我想大部分人都會一笑而過:如何調用?調用就是了。一般情況下是觸發一個單獨的方法或者一個對象的某個方法。但是你應該知道,我既然在這個地方提出這個問題,就肯定不是這樣簡單的答案。
?難點在于如何分析“操作”這個業務過程。其實“操作”分為以下三部分:
- 調用者
- 操作
- 執行者
首先,調用者不一定都是客戶端,可能是一個對象或者集合。例如我們常見的電視遙控器,就是一個典型的調用者對象。
其次,操作和執行者不一樣。操作時,除了真正執行之外,還可能有其他的動作,如緩存、判斷等。
最后,這樣的劃分是為了職責單一和充分解耦。當你的需求可以用簡單的函數調用解決時,那當然最好。但是如果后期隨著系統的升級和變更而變得業務復雜時,就應該考慮用這種設計模式——命令模式。
上圖是命令模式的類圖。左側是的Command和ConcreteCommand是操作(命令)的抽象和實現,這個不重要,我們可以把這兩個統一看成一個“操作”整體。Invoker是調用者,Receiver是真正的執行者。
調用過程是:Invoker.Excute() -> Command.Excute() -> Receiver.Action()。這樣我們還可以在Command中實現一些緩存、判斷之類的業務操作。可以按照自己具體的需求編寫代碼。
具體的代碼這里就不寫了,把這個業務過程理解了,寫代碼也很簡單。重點還是在于理解“操作”(命令)的業務過程,以及在復雜過程下對調用者、操作、執行者之間的解耦。
8.10 ? ? 情況9:一種策略,多種算法
假如上圖是我們系統中一個功能的類圖,定義一個接口,用兩個不同的類去實現。客戶端的代碼調用為:
有出現了比較討厭的條件判斷,任何條件判斷的復雜化都將導致職責的混亂和代碼的臃腫。如果想要解決這種問題,我們需要把這些邏輯判斷分離出來。先定義一個類來封裝客戶端和實現類的直接聯系。
如此一來,客戶端的調用代碼為:
這就是——策略模式。類圖如下:
附:關于這個策略模式,思考了很久也沒有想出一個何時的表達方法,我沒有真正理解它的用處,感覺它說的很對,但是我覺得它沒多少用處。所以,針對這個模式的表述,大家先以參考為主。如果我有了更好的理解方式,再修改。
8.11 ? ? 情況10:簡化一個對象組合的操作
針對一個對象組合,例如一個遞歸的樹形結構,往往對每個節點都會有相同的操作。代碼如下:
?如果對象結構較復雜,而且新增功能較多,代碼將會變得非常臃腫。
解決這個問題時,不好直接去抽象。一來是因為現在已經在一個抽象的結構中,二來也因為每個節點新增的功能,不一定都相同。所以,現在我們最好的方式是將“新增功能”這個未來不確定的事情,交給另外對象去做。先去隔離。
另外定義一個Visitor類,由它來接收一個Element對象,然后執行各種操作。
此時在Element類中,就不需要每次新增功能時,都重寫代碼。只需要在Element類中加入一個方法,該方法將調用Visitor的方法來實現具體的功能。
這就是——訪問者模式,它使你可以在不改變各元素的類的前提下定義作用于這些元素的新操作。
8.12 ? ? 總結
注:Interpreter解釋器模式不常用,暫不說明。
本節介紹了對象行為和對象操作過程中的一些常用業務過程以及其中遇到的問題,同時針對每種情況引入了相應的設計模式。
這塊兒的過程較復雜,梳理起來也比較麻煩,當前的描述和一個系統的流程相比,我想還是有不少差距的。但是相信大家在看到每種情況的時候,或多或少的肯定有過類似的開發經歷,每種情況都是和我們日常的開發工作息息相關的。如果這些介紹起不到一個教程或者引導的作用,那就權當是一次拋磚引玉,或者一次學習交流。
最終還是希望大家能從系統的設計、重構、解決問題的角度去引出設計模式,然后才能真正理解設計模式。
?
9????? 總結
從5.12開始寫,到今天6.4,磕磕絆絆的總算寫完了初稿。雖然不到一個月,但是堅持下來也很不容易。而且這只是一個開始,我想再在這個基礎上繼續寫第二版、第三版,當然還需要再去看更多的書、博客以及結合實際的開發經驗和例證。
且先不說應用,即便是真正理解設計模式,也不是易事,沒有開發經驗、沒有一個有效的方法,學起來也是事倍功半。甚至會像我之前一樣,學一次忘一次。我覺得我現在提出的思路有一定效果,至少對于我是有效的。大家如果有其他建議或者思路,歡迎和我交流 wangfupeng1988$163.com($->@)
總結
以上是生活随笔為你收集整理的换种思路去理解设计模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++源代码免杀之函数的动态调用
- 下一篇: __declspec(selectany