設計模式學習總結
引子
剛開始學習設計模式的時候,感到這些模式真的非常抽象。今年下半年以來,隨著我們組工作重點的轉移,以及我在小組中角色的變化,我開始有條件提出自己對新系統的設計想法。在設計過程中,我發現了很多設計模式的用處,也確實應用了很多設計模式,這讓我越來越感到設計模式的重要性,因此我寫了這十餘篇專門介紹設計模式的文章,作為我的學習筆記。
《設計模式——可複用的面向物件軟體的基礎》(有趣的是,梅巨集一再在組會上強調應該譯成重用)中介紹了一共23種設計模式,我一共寫了19個設計模式(其中三個和在一篇文章中),餘下四個,考慮到該模式的應用範圍我就沒有介紹。在寫這些文章時,其中的很多例子都是我在實踐中提煉出來的,當然也有很大一部分是《設計模式》中的例子。不過,這四個人(四人團)生活的年代裡現在已經很遠了,所以它們的例子也很古老。
讓我們更加設計模式
設計模式是個好東西,它給出了很多設計中的技巧與思路,對於很多優秀的設計,它加以總結與提煉。設計模式並非四人團拍腦瓜想出來的,而是他們蒐集了其他人優秀的設計,加以整理出來的,他們不是這些模式的創造者,僅僅是整理者。
應用設計模式會給我們帶來很多好處:軟體將變得更加靈活,模組之間的耦合度將會降低,效率會提升,開銷會減少。更重要的,設計模式就好像美聲唱法中的花腔,讓你的設計更加漂亮。總的來說,設計模式似乎將軟體設計提升到藝術的層次。
設計模式已經被廣泛的應用了,在現在很多的圖形介面框架都使用了MVC模式,大量跌代器模式的應用,徹底改變了我們對集合的操作方式。不僅如此,應用了設計模式的設計,往往被看成為優秀的設計。這是因為,這些設計模式都是久經考驗的。
模式不是模型
在學習和使用設計模式的時候,往往出現一個非常嚴重的誤區,那就是設計模式必須嚴格地遵守,不能修改。但是設計模式不是設計模型,並非一成不變。正相反,設計模式中最核心的要素並非設計的結構,而是設計的思想。只有掌握住設計模式的核心思想,才能正確、靈活的應用設計模式,否則再怎麼使用設計模式,也不過是生搬硬套。
當然,掌握設計模式的思想,關鍵是要仔細研究模式的意圖和結構。一個模式的意圖,就是使用這個設計模式的目的,體現了為什麼要使用這個模式,也就是需求問題。這個模式的結構,就是如何去解決這個問題,是一種手段、一種經典的解決方法,這種解決方法只是一種建議。兩個方面結合起來,明白為什麼需要設計模式,同時明白瞭如何實現這個模式,就容易抓住模式的本質思想。
在抓住意圖和結構的基礎上,實踐也是掌握設計模式的必要方法。當然,設計模式必須在某個場景下得到應用才有意義,這也是為什麼《設計模式》中提供了大量的例子用來說明模式的應用場景,這實際上為讀者提供了一種上下文環境。學外語不是要強調“語言環境”麼,學習設計模式也是這樣。
不要設計模式
看到網上很多人在討論設計模式,他們確實很有***,滿嘴都是模式的名字,恨不得寫個Hello World都要應用到設計模式。設計模式確實是好東西,但是,中國有句古話叫作物極必反,即便是按照辯證法,事物總要一分為二的看。
我們說設計模式的目的是為了讓軟體更加靈活,重用度更高。但是,某種意義上,設計模式增加了軟體維護的難度,特別是它增加了物件之間關聯的複雜度。
我們總說,重用可以提高軟體開發的效率。如果你是大牛,你自然希望你的設計可以被反覆使用10000年,那就是:當世界毀滅的時候,你的設計依然存在。然而,現實是一個系統的設計往往在5年之內就會被拋棄,這是因為:1,軟體技術產生了新的變化,使用新的技術進行的設計,無論如何都比你的設計好;2,硬體環境發生了很大變化,你的設計裡對開銷或者效率的追求已經沒有意義了;3,新的大牛出現了,並且取代了你的位置。
應用設計模式會導致設計週期的加長(因為更復雜了),但是很多專案還在設計階段就已經胎死腹中,再好的設計也沒有發揮的餘地。當我們向設計模式頂禮膜拜的時候,我們還必須清醒地看到軟體生產中非技術層面上的東西往往具有決定性作用。
理想固然崇高,但現實總是殘酷的。如何看清理想與現實的界限,恐怕是需要我們在實踐中不斷磨礪而體會出來的。在看完設計模式後,不妨反問以下自己,這些模式究竟能給你帶來什麼?
Interpreter、Iterator、State模式
Interpreter模式:這個模式主要試圖去解釋一種語言。如果你學過形式語言,那麼這個模式對你來說是多餘的。
Iterator模式:這個模式試圖隱藏集合的內部表示,又同時可以使使用者依次訪問集合中的元素。現在STL和Java的跌代器就是應用這個模式的結果。
State模式:這個模式的意圖是允許物件在其狀態改變時修改其行為,好像物件改變了。這個模式的應用場景是當物件的行為依賴於物件的狀態時。為了實現這個模式,我們可以為每個狀態下的行為實現一個類,當物件的狀態發生改變,它呼叫不同狀態物件的例項方法。注意,以前可能需要使用switch或者if語句進行分支轉換,現在則利用多型機制完成。
Flyweight模式
這個模式利用共享有效的支援大量的細粒度的物件。比如,編輯軟體中,一篇文章有很多個字元,我們可以對每個字元物件生成一個物件,如果這篇文章有幾M個文字,那麼物件的數量肯定是不能容忍的。使用Flyweight模式,我們將所有的文字物件共享起來,文章中的字元僅僅是指向共享池中的某個物件的索引。
在這裡要搞清楚一件事情,利用Flyweight模式不會有效地減少資訊的數量(也就是軟體的空間開銷),因為無論是否共享,表達這麼多資訊所需要的編碼數量是一定的,所以開銷不會大幅減小。只是,這個模式會減少系統中物件的數量,因為大量的物件會被共享。
在編輯軟體中,字元物件被共享,那麼一篇文章中的文字,可以按照段落、格式等等進行結組,一組文字構成一個物件,這樣物件從單個文字變成一組文字,數量大幅減少。
在使用Flyweight模式需要注意的一點,由於物件被共享了,因此這些物件沒有各自的屬性,那麼根據上下文環境,我們在使用這些物件的時候,必須向它傳遞一些引數。在編輯軟體中,這些引數可能就是字型、字號、顏色等等資訊。
使用Flyweight模式還有一個好處,那就是我們可以在不修改系統的情況下增加享元。
Command模式
Command模式,將一個請求封裝為一個物件。這樣,你可以向客戶端傳送不同請求的引數,排隊或記錄請求,同時可以支援不能執行的請求。
在軟體中,不同的模組、物件之間經常會各種呼叫,或者我們稱之為請求。傳統的方法,我們將請求實現為函式呼叫。這樣做是最簡單的方法,但卻在無形之中增加了模組之間的耦合度。當請求發生很大變化的時候,系統將變得很難維護。與此同時,當服務端(接受請求的一端)增加或者刪除一個請求的時候,按照傳統的方法,客戶端(傳送請求的一端)也必須重新編譯(這一點在刪除請求的時候最明顯),這樣系統才能正確執行。
使用Command模式的一個核心思想就是,服務端提供一個統一的請求處理介面,客戶端則通過呼叫介面向服務端傳送請求,這些請求被封裝成物件的形式(或者其等價形式)。在《設計模式》中,“四人團”並沒有強調統一介面的事情,它強調了另一個方面,那就是封裝請求。事實上,封裝一個請求總是要求有一個地方來接受和處理這個請求的,這個地方實際上就是統一請求介面。
在《設計模式》中,請求被封裝成一個Command物件,這個物件儲存著請求型別、引數等資訊,服務端收到這個命令後就會執行Command物件中的Execute()函式,這個函式具體實現了真正的操作。這種實現方法可以保證增加新的請求而不必重新編譯服務端。
我個人認為,Command模式的另一個形式就是在服務端實現各種操作,Command物件只是負責指明請求的型別,這樣,當伺服器端發現請求不正確時,可以忽略該請求。和上一種形式相比,這種形式更加簡潔(因為可以不真正實現Command物件,在C++中可以使用不定引數實現),但是缺少靈活性。
Command模式使得記錄請求成為了可能,我們可以捕獲系統中的請求物件,記錄他們。
Composite模式
Composite模式的意圖是“將物件組合成樹形結構表示‘整體-部分’的層次結構。Composite使得使用者對單個物件和組合物件的使用更具有一致性”。
在Word中我們經常會將一些圖元進行“組合”,組合以後的圖形還可以向簡單圖元那樣進行移動、變形等等操作;除此以外,在Word中,我們對於一個字元、一個片語、一句話、一個段落,甚至是整篇文章的操作是相同的,我們都可以進行剪下、複製,進行字型與大小的調整,進行顏色的變換。這些例子都是Composite模式的例項,我們將簡單的元素組合成複雜的元素,然後還可以像操作簡單元素那樣操作組合元素。
Composite模式將子元素組織成樹型,實際上,組織成圖型也沒有問題。使用者總是喜歡組合簡單元素,一方面,使用者可以通過這樣的組合來進行抽象,另一方面,使用者可以通過組合化簡繁瑣的操作。Composite模式在各種視覺化編輯軟體中應用得最為廣泛。
另一使用Composite的經典例子是Java的Swing系統。所有的Swing元件都是繼承自一個叫做JComponent的介面,因此,我們對一個JFrame的操作和對一個JButton的操作是一樣的。這同時也使得,JFrame在管理自己的子元素時,它不需要知道他們是一個JButton還是一個JPanel,對它來說,這只是一個JComponent。
實現Composite模式的關鍵是良好設計的介面,人們應該對可能的元素(簡單的、組合的)進行分析,並設計出通用的操作。儘可能的保證介面操作對所有元素都是有意義的,否則就應該將那些只對部分元素有意義的操作下放到子類中。
Proxy模式
按照“四人團”的說法,Proxy模式可以為控制另一個物件而提供一個代理或者佔位符。
這個模式可以使我們在真正需要的時候建立物件,如果我們不需要這個物件,Proxy模式會為我們提供一個佔位符。如果我們有大量這樣消耗很大的物件的時候,我們就可以使用Proxy模式,初始情況下,Proxy模式只會提供佔位符而不會真正建立物件,但是對於使用者來說,他看到是真正的物件而不是一個代理。一旦使用者需要獲得或者更改物件屬性的時候,Proxy模式就會建立該物件,在此之後,我們就可以通過代理訪問真正的物件了。
在Word裡面應該是使用了Proxy模式。開啟一篇含圖的很長的文件時,大部分的圖片都不會被載入,而僅僅是提供佔位符,只有當用戶準備察看這一頁的時候,代理才會真正載入圖片。
和Singleton模式一樣,Proxy模式都是保證我們可以按需分配物件,不同的是,Singleton模式還會保證在全域性範圍內使用同一個物件例項,而Proxy則沒有這個功能。
Visitor模式
按照“四人團”的說法,Visitor模式的意圖為:將元素的操作表示成一種結構。這樣Visitor模式可以使你在不修改元素結構的前提下增加新的操作。
考慮一個連結串列,我們需要一個求得最大元素的操作,這個操作可能是遍歷每個節點,然後求的最大值。這個時候我們又需要一個為每個元素加1的操作,這個操作還需要遍歷每個節點,同時將每個元素加1。與之類似,還會有很多其他的針對元素操作,他們的特點都是要遍歷連結串列。這個時候可以使用Visitor模式,結點類負責依次遍歷,並呼叫Visitor類中的函式,而Visitor類的具體實現則負責完成功能。
這裡需要注意的是,Visitor類只能是一個介面,針對不同的操作需要有不同的具體實現,針對不同的具體元素,需要設計不同的操作。每個元素負責選擇自己應該呼叫的操作,Visitor子類負責實現具體功能。
一個已知的應用是SmallTalk-80的編譯器,在編譯時,編譯器需要建立一棵語法樹。在這個時候,它使用了Visitor模式,針對不同的操作,比如:型別檢查、程式碼生成等操作實現不同的Visitor具體類,Visitor類中針對不同的節點型別提供不同的操作介面,具體的節點負責選擇呼叫哪種介面,這像是一種回撥操作。
Observer模式
按照“四人團”的說法,Observer模式的意圖是“定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新”。
實際應用的例子是,比如建模工具中,若干條線形元素附著在一個塊狀元素上,當塊狀元素的大小、位置發生變化,那些線形元素也需要進行改變,這個時候我們就可以應用Observer模式,在塊狀元素和線形元素之間建立一對多的關係,並利用這一模式進行維護。
Observer模式首先構造一個Observer類,在這個類中具有一個update函式。被依賴的物件擁有它,依賴的物件被註冊到Observer中,當被依賴的物件發生變化的時候,就呼叫update函式更新所有依賴它的物件。更新的方式由update函式具體實現。
還有一個現實中的例子,各個部門之間進行通訊,當一方發出新的資訊時,按照傳統的方法它必須告訴所有其他部門。如果使用Observer模式,那麼產生新訊息的一方只需要告知Observer,由Observer通知其他方面。
Template Method模式
Template Method模式的意圖是:“定義一個操作中的骨架,而將一些步驟延遲到子類中。這使得子類可以不改變一個演算法的結構即可以重定義該演算法的某些特定步驟。
這一模式和Strategy模式似乎和相似,但是他們的關注點不同。策略模式主要用於演算法的替換,但是模板方法模式主要用於演算法中特定步驟地替換。一個應用模板方法模式的例子是資料庫操作。對於資料庫操作可以有很多中,比如查詢、更新。查詢又可以分為連線資料庫、傳送請求命令、解析結果等等步驟。對於不同的資料庫,比如Oracle和SQL2000,它們連線資料庫、命令格式可能有所不同,但是就查詢和更新著兩個操作來說它們的步驟是相同的。這個時候,我們可以應用模板方法模式,為查詢、更新操作建立一個抽象的演算法,具體的步驟交給子類來實現。如果對於策略模式,我們替換的將是查詢和更新著兩個操作。
但是,將Template Method模式和Strategy模式進行類比是危險的,這兩個模式有著很多重要的不同,但這些不同卻又是十分的細微,只能意會不能言傳。
Factory Method模式
這一模式的意圖是:“定義一個用於建立物件的介面,讓子類決定例項化哪一個類。Factory Method是一個類的例項化延遲到其子類。”
這一模式的關鍵是掌握“何時應用這一模式”,事實上我覺得這也是所有設計模式的關鍵。一個已知的應用就是MFC中關於Document和Frame之間的關係。通常在生成一個多文件程式時,VC會為你建立一個Frame類和Document類,你的Frame類可以用來相應OnFileNew訊息,然後建立一個Document物件。但是對於Windows的訊息系統來說,它並不知道使用者程式中建立的Document類有什麼特性,對於它來說,它所看到是CFrame物件和CDocument物件。Factory Method模式可以保證其他物件不需要知道具體物件的型別而管理這些物件,這一模式通常用於制定框架。
這一模式和Abstract Factory模式很相像,事實上Abstract Factory模式可以由一系列Factory Method模式實現。
Strategy模式
Strategy模式的目的就是“定義一系列的演算法,把他們一個個封裝起來,並且使他們可以互相替換。”
如何理解這一模式,首先看下面這個場景:一組資料進行排序,我們可以選擇很多中排序演算法,這個時候我們定義一個排序策略,然後每個排序演算法實現一個具體策略,這樣使用者就可以在幾個不同的排序演算法中隨意選擇和替換了。
當然,上面的例子中使用策略模式似乎多此一舉,那麼假設遊戲中的敵人的AI,根據玩家的設定可以有不同的級別。在這種情況下,使用策略模式就是十分必要的了。
Bridge模式
按照“四人團”的說法,Bridge模式的意圖是:將抽象部分與他的實現部分分離,使得他們可以獨立的變化。你一定會感到一陣眩暈,不明白這是什麼意思。
首先應該說明的是“抽象”與“實現”的含義。在剛才的那句話中,“抽象”與“實現”並不是我們在描述類結構時所說的“介面”與它的“實現”,或者在Java中抽象類與他的實現。在這裡,“抽象”與“實現”只得是某種工作,“抽象”是說如何完成這項工作,“實現”是說“抽象”中所用的步驟的實現。
一個例子可以很好的說明“抽象”與“實現”的關係。我們編寫一個遊戲,這個遊戲有兩個版本,DX版本和OpenGL版本。我們如何編寫這兩個版本呢?一種方法是我們在這兩個引擎上開發兩套獨立的遊戲,但這顯然不是最好的方法。另一個選擇是我們將遊戲的“抽象”部分與“實現”部分分離,開發一套“抽象”部分,開發兩套“實現”部分。那麼什麼是遊戲的“抽象”部分?很顯然就是遊戲的繪圖(也許用更專業詞彙的應該是:渲染)過程,例如我們如何渲染遊戲的人物,這個人物可能是由很多個多邊形組合而成的,我們按照一定的方法渲染之後,就可以畫出一個人物來。這一部分就可以看作是“抽象”。那麼另一方面就是“實現”部分,在上面的例子中,“實現”部分就是如何繪製基礎的線條、填充顏色,甚至是初始化螢幕等等。這些“實現”和具體的引擎密切相關。
為什麼說我們可以將“抽象”和“實現”分離,使得他們可以各自變化呢?假設現在要開發新的遊戲,或者這個遊戲升級了,在其中出現了新的人物,那麼“抽象”部分就發生了變化,但是具體“實現”沒有變化,因此這個遊戲還可以繼續在你的計算機上執行。另一方面,如果遊戲需要進行移植,目標平臺的圖形系統發生了變化,你可能需要使用新的繪圖引擎,這個時候,你只需要利用新的引擎實現基本的“實現”操作,原始的程式就可以在新的平臺上執行(略去重新編譯的問題)。
Facade模式
Facade模式的目的就是給子系統提供一個統一的介面。現在的軟體都是按照模組進行劃分,對不同的模組分別進行程式設計。但是在使用者開來,不同的模組應該具有統一的介面,換句話說,我們應該可以通過統一的介面訪問系統中的所有功能。
有一個很典型的例子就是編譯系統。通常我們將編譯系統分解為:Compile和Link兩個步驟。一個Compile又可以分解為詞法分析、語法分析、語義分析、中間程式碼生成等等步驟。對於使用者來講,我們不可能將這些模組分別提供給他們,讓他們依次呼叫。相反的,我們應該提供一個統一的介面,使得使用者可以方便的使用各個功能,例如IDE 。
Facade模式在強調模組化開發的同時也強調模組的統一,統一的介面也有利於子系統中模組內部的變化。對於開發大型系統來說,Facade模式是不可缺少的。
Decorator模式
按照“四人團”的說法,Decorator模式的意圖是:動態的給一個物件新增一些額外的職責。值得注意的是,這個物件不知道他增加的是什麼職責。
這個模式的一個典型應用例項是:Java的流。一個檔案流(Java.IO.File)用於讀寫檔案,如果你想使用檔案緩衝,你可在為File新增一個BufferedInputStream或者BufferedOutputStream外觀,這樣這個檔案流就具有了緩衝。再如一個Reader類,你可以給他增加緩衝BufferedReader,然後你還可以給這個緩衝流增加一些格式化讀取的能力。
Decorator模式可以動態的增加物件的額外的職責,這也有利於將額外的功能分別實現,使得使用者可以自由組合。
Adapter模式
有一天你在網上找到一個庫,你打算把它應用到你的程式當中去,但是你發現這個庫的函式不符合你的風格,你會怎麼辦?一個很簡單的方法就是使用Adapter模式。
Adapter模式的目的就是將一個類的介面轉換為使用者希望的介面,使得由於介面不相容而不能一起工作的各個類可以一起工作。
例如在一個軟體裡面可能使用了以前一個版本的類庫。不幸的是這個類庫的效率極高卻和現在的介面不相容,為了繼續複用這個類庫我們就可以使用Adapter模式,在原來的類庫和現在的介面中間實現一個介面卡,使得我們可以用現在的結構呼叫以前的類庫。
例如一個繪圖程式(這種事情總是出現在這類程式中),以前的類庫中提供繪製直線的方法DrawLine,但是新的介面要求繪圖系統還要提供繪製矩形、折線形的方法,為了複用這個類庫,我們實現一個Adapter類,這個類中利用以前的繪圖系統提供的方法實現了新的介面功能。
Singleton模式
這可能是最簡單的一個模式了,但是他的應用卻是最多的。這個模式的目的就是保證一個物件只有一個例項,並且提供一個全域性的訪問點。
那麼這個模式的怎麼實現呢?很簡單,你首先必須為這個類設定一個指標(Java中是引用),然後提供一個方法用來獲得這個類的例項。在這個方法中首先判斷這個指標是否為空,如果是,則建立一個例項,否則直接返回這個指標。
雖然我們可以提供一個全域性訪問點,但實際上這個模式也可以應用到區域性。應用這個模式一個好處就是可以“按需分配”,同時也封裝了物件的獲取過程。不論如何,我覺得應該儘可能的應用這個模式,雖然這會讓你感到很煩……
這個模式在實現過程中可以進行變化,例如在Instance()方法上新增引數Boolean bAlloc,用於指定當例項不存在的時候是否進行建立。這樣做是考慮到,有些時候我們獲得例項的目的不是為了修改,而是為了讀取。這個時候,返回一個空例項和返回一個沒有被修改過的例項在邏輯上是相同的。例如,這個物件是一個數組時,一個“空陣列”和一個“空白的陣列”是相同的。
Builder模式
按照“四人團”的說法,Builder模式的目的是:將一個複雜物件的構建與他的表示分離,使得同樣的構建過程可以建立不同的表示。
一個典型的例子是:檔案的格式轉換。假設一個RTF檔案,我們可以將它轉換成不同的格式,比如TXT、DOC、PDF等等。在這些目標格式的檔案中,有些檔案格式中保留文字字型(比如DOC),有些可能不保留(比如TXT)。當我們開始轉換過程時,按照RTF檔案自己的格式進行分析轉換。轉換的過程是一樣的,但是不同的目標檔案格式對於不同的轉換請求的處理是不同的,比如TXT檔案轉換將會忽略所有的文字格式控制符,但是DOC檔案將會把RTF的控制符轉換為自己的控制符。應用Builder模式,我們可以實現不同的具體生成器,對於相同的請求產生不同的結果。
Director負責向Builder傳送不同的生成請求,在剛才的例子中,RTF文件的分析器可以看作是Director,文件轉換器可以看作是Builder。另一個可以想到的例子是一個編成開發環境,我們可能對源程式進行語法分析,但是分析的目的可能不同,有的分析可能是用來生成程式碼,有的可能是用來形成智慧感知。不論如何,語法分析的過程是相同的,因此將語法分析看成一個Director,程式碼生成和形成智慧感知看作是兩個ConcreteBuilder,對於相同的分析請求產生不同的動作。例如當分析器發現一個函式以後,就會向Builder傳送請求,如果這個Builder例項是程式碼生成器,那麼它可能會記錄這個函式的入口地址,如果這個Builder例項是智慧感知器,那麼它可能像資料庫中插入這個函式的資訊。
Abstract Factory模式
這個模式的關鍵就是設計一個AbstractFactory介面,這個介面提供了一系列抽象函式用來建立各種物件。這個模式的意圖就是使用一個統一的介面用以建立不同具體物件。
假設這樣一個場景,在不同配置的計算機上完成顯示和列印任務,對於高配置的計算機我們使用高解析度的驅動,在低配置上的機器使用低解析度的驅動。這個時候,我們可以將顯示驅動看成ProductA,列印驅動看成ProductB;每種驅動具有兩種解析度,分別對應高低兩種配置。這個時候在建立(配置)系統驅動的時候,我們就可以使用AbstractFactory模式,為高低兩種配置實現兩個具體工廠類,分別用於建立對應的驅動。所有的使用者不用關心當前是什麼配置的計算機,它只需要呼叫統一的抽象工廠介面就可以獲得對應的驅動。
使用這個模式還需要注意的是,產品物件也必須具有良好的設計,以至於使用者不需要關心具體的產品是哪種型別就可以通過抽象產品的介面使用這個產品。使用這個模式可以令使用者無需關心具體環境,降低程式碼耦合度,使得程式結構更加清晰。缺陷是當每個產品的具體實現有很多種時,實現的具體工廠類的數量會迅速膨脹,而且它不能對環境的改變進行立即響應。