筆記--設計模式精解c++-GoF 23 種設計模式解析

0  引言
0.1 設計模式解析(總序)
0.2 設計模式解析後記
0.3  與作者聯繫
1  創建型模式
1.1 Factory模式
 1)爲了提高內聚(Cohesion)和鬆耦合(Coupling) ,我們經常會抽象出一些類的公共接口以形成抽象基類或者接口。 這樣我們可以通過聲明一個指向基類的指針來指向實際的子類實現,達到了多態的目的。這裏很容易出現的一個問題 n 多的子類繼承自抽象基類,我們不得不在每次要用到子類的地方就編寫諸如 new  ×××;的代碼。這裏帶來兩個問題 1)客戶程序員必須知道實際子類的名稱(當系統複雜後,命名將是一個很不好處理的問題,爲了處理可能的名字衝突,有的命名可能並不是具有很好的可讀性和可記憶性,就姑且不論不同程序員千奇百怪的個人偏好了。 ) ,2)程序的擴展性和維護變得越來越困難。
 2)還有一種情況就是在父類中並不知道具體要實例化哪一個具體的子類。這裏的意思爲:假設我們在類 A 中要使用到類 B,B 是一個抽象父類,在 A 中並不知道具體要實例化那一個 B 的子類,但是在類 A的子類 D中是可以知道的。在 A中我們沒有辦法直接使用類似於 new  ×××的語句,因爲根本就不知道×××是什麼。   以上兩個問題也就引出了 Factory模式的兩個最重要的功能:  
 1)定義創建對象的接口,封裝了對象的創建;
 2)使得具體化類的工作延遲到了子類中。
     Factory模式也帶來至少以下兩個問題:
      1)如果爲每一個具體的 ConcreteProduct 類的實例化提供一個函數體,那麼我們可能不得不在系統中添加了一個方法來處理這個新建的 ConcreteProduct,這樣 Factory 的接口永遠就不肯能封閉(Close) 。當然我們可以通過創建一個 Factory的子類來通過多態實現這一點,但是這也是以新建一個類作爲代價的。
    2)在實現中我們可以通過參數化工廠方法,即給 FactoryMethod()傳遞一個參數用以決定是創建具體哪一個具體的 Product(實際上筆者在 VisualCMCS 中也正是這樣做的) 。當然也可以通過模板化避免 1)中的子類創建子類,其方法就是將具體 Product 類作爲模板參數,實現起來也很簡單。
      可以看出,Factory 模式對於對象的創建給予開發人員提供了很好的實現策略,但是Factory 模式僅僅侷限於一類類(就是說 Product 是一類,有一個共同的基類) ,如果我們要爲不同類的類提供一個對象創建的接口,那就要用abstractFactory了。
 
AbstractFactory模式就是用來解決這類問題的:要創建一組相關或者相互依賴的對象。
1.2 AbstactFactory模式
 AbstractFactory 模式關鍵就是將這一組對象的創建封裝到一個用於創建對象的類
(ConcreteFactory) 中, 維護這樣一個創建類總比維護 n 多相關對象的創建過程要簡單的多。 
 AbstractFactory 模式和 Factory 模式的區別是初學(使用)設計模式時候的一個容易引起困惑的地方。實際上,AbstractFactory模式是爲創建一組(有多類)相關或依賴的對象提供創建接口,而 Factory模式正如我在相應的文檔中分析的是爲一類對象提供創建接口或延遲對象的創建到子類中實現。並且可以看到,abstractFactory 模式通常都是使用 Factory 模式實現(ConcreteFactory1)。

1.3 Singleton模式
  個人認爲 Singleton 模式是設計模式中最爲簡單、最爲常見、最容易實現,也是最應該熟悉和掌握的模式。且不說公司企業在招聘的時候爲了考察員工對設計的瞭解和把握,考的最多的就是 Singleton 模式。
 Singleton 模式解決問題十分常見,我們怎樣去創建一個唯一的變量(對象)?在基於對象的設計中我們可以通過創建一個全局變量(對象)來實現,在面向對象和麪向過程結合的設計範式(如 C++中)中,我們也還是可以通過一個全局變量實現這一點。但是當我們遇到了純粹的面向對象範式中,這一點可能就只能是通過 Singleton 模式來實現了,可能這也正是很多公司在招聘 Java 開發人員時候經常考察 Singleton 模式的緣故吧。  Singleton 模式在開發中非常有用,具體使用在討論給出。
     在 Singleton 模式的結構圖中可以看到,我們通過維護一個 static 的成員變量來記錄這 個唯一的對象實例。通過提供一個 staitc 的接口instance 來獲得這個唯一的實例。
 Singleton 模式的實現無須補充解釋,需要說明的是,Singleton 不可以被實例化,因此我們將其構造函數聲明爲 protected或者直接聲明爲 private。
 Singleton 模式在開發中經常用到,且不說我們開發過程中一些變量必須是唯一的,比如說打印機的實例等等。
 Singleton 模式經常和 Factory(AbstractFactory)模式在一起使用,因爲系統中工廠對象一般來說只要一個,筆者在開發 Visual CMCS 的時候,語義分析過程(以及其他過程)中都用到工廠模式來創建對象(對象實在是太多了) ,這裏的工廠對象實現就是同時是一個Singleton 模式的實例,因爲系統我們就只要一個工廠來創建對象就可以了。
int main(int argc,char* argv[])
{
  Singleton* sgn = Singleton::Instance(); 
 return 0;
}

1.4 Builder模式
  生活中有着很多的Builder的例子, 個人覺得大學生活就是一個Builder模式的最好體驗:要完成大學教育,一般將大學教育過程分成 4 個學期進行,因此沒有學習可以看作是構建完整大學教育的一個部分構建過程,每個人經過這 4 年的(4個階段)構建過程得到的最後的結果不一樣,因爲可能在四個階段的構建中引入了很多的參數(每個人的機會和際遇不完全相同) 。
 Builder 模式要解決的也正是這樣的問題:當我們要創建的對象很複雜的時候(通常是由很多其他的對象組合而成) ,我們要要複雜對象的創建過程和這個對象的表示(展示)分離開來,這樣做的好處就是通過一步步的進行復雜對象的構建,由於在每一步的構造過程中可以引入參數,使得經過相同的步驟創建最後得到的對象的展示不一樣。
Builder模式和AbstractFactory模式在功能上很相似,因爲都是用來創建大的複雜的對象,它們的區別是:Builder模式強調的是一步步創建對象,並通過相同的創建過程可以獲得不同的結果對象,一般來說Builder模式中對象不是直接返回的。而在AbstractFactory模式中對象是直接返回的,AbstractFactory模式強調的是爲創建多個相互依賴的對象提供一個同一的接口。
1.5 Prototype模式
Prototype模式也正是提供了自我複製的功能,就是說新對象的創建可以通過已有對象進行創建。在C++中拷貝構造函數(Copy Constructor)曾經是很對程序員的噩夢,淺層拷貝和深層拷貝的魔魘也是很多程序員在面試時候的快餐和系統崩潰時候的根源之一。
Prototype模式提供了一個通過已存在對象進行新對象創建的接口(Clone),Clone()實現和具體的實現語言相關,在C++中我們將通過拷貝構造函數實現之。

Prototype模式的結構和實現都很簡單,其關鍵就是(C++中)拷貝構造函數的實現方式,這也是C++實現技術層面上的事情。由於在示例代碼中不涉及到深層拷貝(主要指有指針、複合對象的情況),因此我們通過編譯器提供的默認的拷貝構造函數(按位拷貝)的方式進行實現。說明的是這一切只是爲了實現簡單起見,也因爲本文檔的重點不在拷貝構造函數的實現技術,而在Prototype模式本身的思想。
Prototype模式通過複製原型(Prototype)而獲得新對象創建的功能,這裏Prototype本身就是“對象工廠”(因爲能夠生產對象),實際上Prototype模式和Builder模式、AbstractFactory模式都是通過一個類(對象實例)來專門負責對象的創建工作(工廠對象),它們之間的區別是:Builder模式重在複雜對象的一步步創建(並不直接返回對象),AbstractFactory模式重在產生多個相互依賴類的對象,而Prototype模式重在從自身複製自己創建新類。
2  結構型模式
2.1 Bridge模式
總結面向對象實際上就兩句話:一是鬆耦合(Coupling),二是高內聚(Cohesion)。面向對象系統追求的目標就是儘可能地提高系統模塊內部的內聚(Cohesion)、儘可能降低模塊間的耦合(Coupling)。然而這也是面向對象設計過程中最爲難把握的部分,大家肯定在OO系統的開發過程中遇到這樣的問題:
1)客戶給了你一個需求,於是使用一個類來實現(A);
2)客戶需求變化,有兩個算法實現功能,於是改變設計,我們通過一個抽象的基類,再定義兩個具體類實現兩個不同的算法(A1和A2);
3)客戶又告訴我們說對於不同的操作系統,於是再抽象一個層次,作爲一個抽象基類A0,在分別爲每個操作系統派生具體類(A00和A01,其中A00表示原來的類A)實現不同操作系統上的客戶需求,這樣我們就有了一共4個類。
4)可能用戶的需求又有變化,比如說又有了一種新的算法……..
5)我們陷入了一個需求變化的鬱悶當中,也因此帶來了類的迅速膨脹。
Bridge模式則正是解決了這類問題。

在Bridge模式的結構圖中可以看到,系統被分爲兩個相對獨立的部分,左邊是抽象部分,右邊是實現部分,這兩個部分可以互相獨立地進行修改:例如上面問題中的客戶需求變化,當用戶需求需要從Abstraction派生一個具體子類時候,並不需要像上面通過繼承方式實現時候需要添加子類A1和A2了。另外當上面問題中由於算法添加也只用改變右邊實現(添加一個具體化子類),而右邊不用在變化,也不用添加具體子類了。
Bridge是設計模式中比較複雜和難理解的模式之一,也是OO開發與設計中經常會用到的模式之一。使用組合(委託)的方式將抽象和實現徹底地解耦,這樣的好處是抽象和實現可以分別獨立地變化,系統的耦合性也得到了很好的降低。
GoF在說明Bridge模式時,在意圖中指出Bridge模式“將抽象部分與它的實現部分分離,使得它們可以獨立地變化”。這句話很簡單,但是也很複雜,連Bruce Eckel在他的大作《Thinking in Patterns》中說“Bridge模式是GoF所講述得最不好(Poorly-described)的模式”,個人覺得也正是如此。原因就在於GoF的那句話中的“實現”該怎麼去理解:“實現”特別是和“抽象”放在一起的時候我們“默認”的理解是“實現”就是“抽象”的具體子類的實現,但是這裏GoF所謂的“實現”的含義不是指抽象基類的具體子類對抽象基類中虛函數(接口)的實現,是和繼承結合在一起的。而這裏的“實現”的含義指的是怎麼去實現用戶的需求,並且指的是通過組合(委託)的方式實現的,因此這裏的實現不是指的繼承基類、實現基類接口,而是指的是通過對象組合實現用戶的需求。理解了這一點也就理解了Bridge模式,理解了Bridge模式,你的設計就會更加Elegant了。
實際上上面使用Bridge模式和使用帶來問題方式的解決方案的根本區別在於是通過繼承還是通過組合的方式去實現一個功能需求。因此面向對象分析和設計中有一個原則就是:Favor Composition Over Inheritance。其原因也正在這裏。
2.2 Adapter模式
實際上在軟件系統設計和開發中,這種問題也會經常遇到:我們爲了完成某項工作購買了一個第三方的庫來加快開發。這就帶來了一個問題:我們在應用程序中已經設計好了接口,與這個第三方提供的接口不一致,爲了使得這些接口不兼容的類(不能在一起工作)可以在一起工作了,Adapter模式提供了將一個類(第三方庫)的接口轉化爲客戶(購買使用者)希望的接口。
在上面生活中問題的解決方式也就正好對應了Adapter模式的兩種類別:類模式和對象模式。
在Adapter模式的結構圖中可以看到,類模式的Adapter採用繼承的方式複用Adaptee的接口,而在對象模式的Adapter中我們則採用組合的方式實現Adaptee的複用。有關這些具體的實現和分析將在代碼說明和討論中給出。
Adapter模式實現上比較簡單,要說明的是在類模式Adapter中,我們通過private繼承Adaptee獲得實現繼承的效果,而通過public繼承Target獲得接口繼承的效果(有關實現繼承和接口繼承參見討論部分)。
在Adapter模式的兩種模式中,有一個很重要的概念就是接口繼承和實現繼承的區別和聯繫。接口繼承和實現繼承是面向對象領域的兩個重要的概念,接口繼承指的是通過繼承,子類獲得了父類的接口,而實現繼承指的是通過繼承子類獲得了父類的實現(並不統共接口)。在C++中的public繼承既是接口繼承又是實現繼承,因爲子類在繼承了父類後既可以對外提供父類中的接口操作,又可以獲得父類的接口實現。當然我們可以通過一定的方式和技術模擬單獨的接口繼承和實現繼承,例如我們可以通過private繼承獲得實現繼承的效果(private繼承後,父類中的接口都變爲private,當然只能是實現繼承了。),通過純抽象基類模擬接口繼承的效果,但是在C++中pure virtual function也可以提供默認實現,因此這是不純正的接口繼承,但是在Java中我們可以interface來獲得真正的接口繼承了.
2.3 Decorator模式
在OO設計和開發過程,可能會經常遇到以下的情況:我們需要爲一個已經定義好的類添加新的職責(操作),通常的情況我們會給定義一個新類繼承自定義好的類,這樣會帶來一個問題(將在本模式的討論中給出)。通過繼承的方式解決這樣的情況還帶來了系統的複雜性,因爲繼承的深度會變得很深。
而Decorator提供了一種給類增加職責的方法,不是通過繼承實現的,而是通過組合。
在結構圖中,ConcreteComponent和Decorator需要有同樣的接口,因此ConcreteComponent和Decorator有着一個共同的父類。這裏有人會問,讓Decorator直接維護一個指向ConcreteComponent引用(指針)不就可以達到同樣的效果,答案是肯定並且是否定的。肯定的是你可以通過這種方式實現,否定的是你不要用這種方式實現,因爲通過這種方式你就只能爲這個特定的ConcreteComponent提供修飾操作了,當有了一個新的ConcreteComponent你又要去新建一個Decorator來實現。但是通過結構圖中的ConcreteComponent和Decorator有一個公共基類,就可以利用OO中多態的思想來實現只要是Component型別的對象都可以提供修飾操作的類,這種情況下你就算新建了100個Component型別的類ConcreteComponent,也都可以由Decorator一個類搞定。這也正是Decorator模式的關鍵和威力所在了。
當然如果你只用給Component型別類添加一種修飾,則Decorator這個基類就不是很必要了。
Decorator模式和Composite模式有相似的結構圖,其區別在Composite模式已經詳細討論過了,請參看相應文檔。另外GoF在《設計模式》中也討論到Decorator和Proxy模式有很大程度上的相似,初學設計模式可能實在看不出這之間的一個聯繫和相似,並且它們在結構圖上也很不相似。實際上,在本文檔2.2節模式選擇中分析到,讓Decorator直接擁有一個ConcreteComponent的引用(指針)也可以達到修飾的功能,大家再把這種方式的結構圖畫出來,就和Proxy很相似了!
Decorator模式和Proxy模式的相似的地方在於它們都擁有一個指向其他對象的引用(指針),即通過組合的方式來爲對象提供更多操作(或者Decorator模式)間接性(Proxy模式)。
但是他們的區別是,Proxy模式會提供使用其作爲代理的對象一樣接口,使用代理類將其操作都委託給Proxy直接進行。這裏可以簡單理解爲組合和委託之間的微妙的區別了。
Decorator模式除了採用組合的方式取得了比採用繼承方式更好的效果,Decorator模式還給設計帶來一種“即用即付”的方式來添加職責。在OO設計和分析經常有這樣一種情況:爲了多態,通過父類指針指向其具體子類,但是這就帶來另外一個問題,當具體子類要添加新的職責,就必須向其父類添加一個這個職責的抽象接口,否則是通過父類指針是調用不到這個方法了。這樣處於高層的父類就承載了太多的特徵(方法),並且繼承自這個父類的所有子類都不可避免繼承了父類的這些接口,但是可能這並不是這個具體子類所需要的。而在Decorator模式提供了一種較好的解決方法,當需要添加一個操作的時候就可以通過Decorator模式來解決,你可以一步步添加新的職責。
2.4 Composite模式
在開發中,我們經常可能要遞歸構建樹狀的組合結構,Composite模式則提供了很好的解決方案。
Composite模式在實現中有一個問題就是要提供對於子節點(Leaf)的管理策略,這裏使用的是STL 中的vector,可以提供其他的實現方式,如數組、鏈表、Hash表等.
Composite模式通過和Decorator模式有着類似的結構圖,但是Composite模式旨在構造類,而Decorator模式重在不生成子類即可給對象添加職責。Decorator模式重在修飾,而Composite模式重在表示。
2.5 Flyweight模式
在面向對象系統的設計何實現中,創建對象是最爲常見的操作。這裏面就有一個問題:如果一個應用程序使用了太多的對象,就會造成很大的存儲開銷。特別是對於大量輕量級(細粒度)的對象,比如在文檔編輯器的設計過程中,我們如果爲沒有字母創建一個對象的話,系統可能會因爲大量的對象而造成存儲開銷的浪費。例如一個字母“a”在文檔中出現了100000次,而實際上我們可以讓這一萬個字母“a”共享一個對象,當然因爲在不同的位置可能字母“a”有不同的顯示效果(例如字體和大小等設置不同),在這種情況我們可以爲將對象的狀態分爲“外部狀態”和“內部狀態”,將可以被共享(不會變化)的狀態作爲內部狀態存儲在對象中,而外部對象(例如上面提到的字體、大小等)我們可以在適當的時候將外部對象最爲參數傳遞給對象(例如在顯示的時候,將字體、大小等信息傳遞給對象)。
可以從圖2-1中看出,Flyweight模式中有一個類似Factory模式的對象構造工廠FlyweightFactory,當客戶程序員(Client)需要一個對象時候就會向FlyweightFactory發出請求對象的消息GetFlyweight()消息,FlyweightFactory擁有一個管理、存儲對象的“倉庫”(或者叫對象池,vector實現),GetFlyweight()消息會遍歷對象池中的對象,如果已經存在則直接返回給Client,否則創建一個新的對象返回給Client。當然可能也有不想被共享的對象(例如結構圖中的UnshareConcreteFlyweight),但不在本模式的講解範圍,故在實現中不給出。
Flyweight模式在實現過程中主要是要爲共享對象提供一個存放的“倉庫”(對象池),這裏是通過C++ STL中Vector容器,當然就牽涉到STL編程的一些問題(Iterator使用等)。另外應該注意的就是對對象“倉庫”(對象池)的管理策略(查找、插入等),這裏是通過直接的順序遍歷實現的,當然我們可以使用其他更加有效的索引策略,例如Hash表的管理策略,當時這些細節已經不是Flyweight模式本身要處理的了。
我們在State模式和Strategy模式中會產生很多的對象,因此我們可以通過Flyweight模式來解決這個問題。
2.6 Facade模式
實際上在軟件系統開發中也經常回會遇到這樣的情況,可能你實現了一些接口(模塊),而這些接口(模塊)都分佈在幾個類中(比如A和B、C、D):A中實現了一些接口,B中實現一些接口(或者A代表一個獨立模塊,B、C、D代表另一些獨立模塊)。然後你的客戶程序員(使用你設計的開發人員)只有很少的要知道你的不同接口到底是在那個類中實現的,絕大多數只是想簡單的組合你的A-D的類的接口,他並不想知道這些接口在哪裏實現的。
這裏的客戶程序員就是上面生活中想辦理手續的鬱悶的人!在現實生活中我們可能可以很快想到找一個人代理所有的事情就可以解決你的問題(你只要維護和他的簡單的一個接口而已了!),在軟件系統設計開發中我們可以通過一個叫做Façade的模式來解決上面的問題。
2.7 Proxy模式
至少在以下集中情況下可以用Proxy模式解決問題:
1)創建開銷大的對象時候,比如顯示一幅大的圖片,我們將這個創建的過程交給代理去完成,GoF稱之爲虛代理(Virtual Proxy);
2)爲網絡上的對象創建一個局部的本地代理,比如要操作一個網絡上的一個對象(網絡性能不好的時候,問題尤其突出),我們將這個操縱的過程交給一個代理去完成,GoF稱之爲遠程代理(Remote Proxy);
3)對對象進行控制訪問的時候,比如在Jive論壇中不同權限的用戶(如管理員、普通用戶等)將獲得不同層次的操作權限,我們將這個工作交給一個代理去完成,GoF稱之爲保護代理(Protection Proxy)。
4)智能指針(Smart Pointer),關於這個方面的內容,建議參看Andrew Koenig的《C++沉思錄》中的第5章。

Proxy模式最大的好處就是實現了邏輯和實現的徹底解耦。
3  行爲模式
3.1 Template模式
在面向對象系統的分析與設計過程中經常會遇到這樣一種情況:對於某一個業務邏輯(算法實現)在不同的對象中有不同的細節實現,但是邏輯(算法)的框架(或通用的應用算法)是相同的。Template提供了這種情況的一個實現框架。
Template模式是採用繼承的方式實現這一點:將邏輯(算法)框架放在抽象基類中,並定義好細節的接口,子類中實現細節。【註釋1】
【註釋1】:Strategy模式解決的是和Template模式類似的問題,但是Strategy模式是將邏輯(算法)封裝到一個類中,並採取組合(委託)的方式解決這個問題。
解決2.1中問題可以採取兩種模式來解決,一是Template模式,二是Strategy模式。本文當給出的是Template模式。一個通用的Template模式的結構圖爲:
3.2 Strategy模式
Template模式實際上就是利用面向對象中多態的概念實現算法實現細節和高層接口的鬆耦合。可以看到Template模式採取的是繼承方式實現這一點的,由於繼承是一種強約束性的條件,因此也給Template模式帶來一些許多不方便的地方(有關這一點將在討論中展開)。
由於Template模式的實現代碼很簡單,因此解釋是多餘的。其關鍵是將通用算法(邏輯)封裝起來,而將算法細節讓子類實現(多態)。
唯一注意的是我們將原語操作(細節算法)定義未保護(Protected)成員,只供模板方法調用(子類可以)。
Template模式是很簡單模式,但是也應用很廣的模式。如上面的分析和實現中闡明的Template是採用繼承的方式實現算法的異構,其關鍵點就是將通用算法封裝在抽象基類中,並將不同的算法細節放到子類中實現。
Template模式獲得一種反向控制結構效果,這也是面向對象系統的分析和設計中一個原則DIP(依賴倒置:Dependency Inversion Principles)。其含義就是父類調用子類的操作(高層模塊調用低層模塊的操作),低層模塊實現高層模塊聲明的接口。這樣控制權在父類(高層模塊),低層模塊反而要依賴高層模塊。
繼承的強制性約束關係也讓Template模式有不足的地方,我們可以看到對於ConcreteClass類中的實現的原語方法Primitive1(),是不能被別的類複用。假設我們要創建一個AbstractClass的變體AnotherAbstractClass,並且兩者只是通用算法不一樣,其原語操作想複用AbstractClass的子類的實現。但是這是不可能實現的,因爲ConcreteClass繼承自AbstractClass,也就繼承了AbstractClass的通用算法,AnotherAbstractClass是復用不了ConcreteClass的實現,因爲後者不是繼承自前者。
Template模式暴露的問題也正是繼承所固有的問題,Strategy模式則通過組合(委託)來達到和Template模式類似的效果,其代價就是空間和時間上的代價,關於Strategy模式的詳細討論請參考Strategy模式解析。
Strategy模式和Template模式要解決的問題是相同(類似)的,都是爲了給業務邏輯(算法)具體實現和抽象接口之間的解耦。Strategy模式將邏輯(算法)封裝到一個類(Context)裏面,通過組合的方式將具體算法的實現在組合對象中實現,再通過委託的方式將抽象接口的實現委託給組合對象實現。State模式也有類似的功能,他們之間的區別將在討論中給出。
這裏的關鍵就是將算法的邏輯抽象接口(DoAction)封裝到一個類中(Context),再通過委託的方式將具體的算法實現委託給具體的Strategy類來實現(ConcreteStrategeA類)。
可以看到Strategy模式和Template模式解決了類似的問題,也正如在Template模式中分析的,Strategy模式和Template模式實際是實現一個抽象接口的兩種方式:繼承和組合之間的區別。要實現一個抽象接口,繼承是一種方式:我們將抽象接口聲明在基類中,將具體的實現放在具體子類中。組合(委託)是另外一種方式:我們將接口的實現放在被組合對象中,將抽象接口放在組合類中。這兩種方式各有優缺點,先列出來:
1) 繼承:
􀂄 優點
1)易於修改和擴展那些被複用的實現。
􀂄 缺點
1)破壞了封裝性,繼承中父類的實現細節暴露給子類了;
2)“白盒”複用,原因在1)中;
3)當父類的實現更改時,其所有子類將不得不隨之改變
4)從父類繼承而來的實現在運行期間不能改變(編譯期間就已經確定了)。
2) 組合
􀂄 優點
1)“黑盒”複用,因爲被包含對象的內部細節對外是不可見的;
2)封裝性好,原因爲1);
3)實現和抽象的依賴性很小(組合對象和被組合對象之間的依賴性小);
4)可以在運行期間動態定義實現(通過一個指向相同類型的指針,典型的是抽象基類的指針)。
􀂄 缺點
1)系統中對象過多。
從上面對比中我們可以看出,組合相比繼承可以取得更好的效果,因此在面向對象的設計中的有一條很重要的原則就是:優先使用(對象)組合,而非(類)繼承(Favor Composition Over Inheritance)。
實際上,繼承是一種強制性很強的方式,因此也使得基類和具體子類之間的耦合性很強。例如在Template模式中在ConcreteClass1中定義的原語操作別的類是不能夠直接複用(除非你繼承自AbstractClass,具體分析請參看Template模式文檔)。而組合(委託)的方式則有很小的耦合性,實現(具體實現)和接口(抽象接口)之間的依賴性很小,例如在本實現中,ConcreteStrategyA的具體實現操作很容易被別的類複用,例如我們要定義另一個Context類AnotherContext,只要組合一個指向Strategy的指針就可以很容易地複用ConcreteStrategyA的實現了。
我們在Bridge模式的問題和Bridge模式的分析中,正是說明了繼承和組合之間的區別。請參看相應模式解析。
另外Strategy模式很State模式也有相似之處,但是State模式注重的對象在不同的狀態下不同的操作。兩者之間的區別就是State模式中具體實現類中有一個指向Context的引用,而Strategy模式則沒有。具體分析請參看相應的State模式分析中。
3.3 State模式
每個人、事物在不同的狀態下會有不同表現(動作),而一個狀態又會在不同的表現下轉移到下一個不同的狀態(State)。最簡單的一個生活中的例子就是:地鐵入口處,如果你放入正確的地鐵票,門就會打開讓你通過。在出口處也是驗票,如果正確你就可以ok,否則就不讓你通過(如果你動作野蠻,或許會有報警(Alarm),:))。
有限狀態自動機(FSM)也是一個典型的狀態不同,對輸入有不同的響應(狀態轉移)。通常我們在實現這類系統會使用到很多的Switch/Case語句,Case某種狀態,發生什麼動作,Case另外一種狀態,則發生另外一種狀態。但是這種實現方式至少有以下兩個問題:
1)當狀態數目不是很多的時候,Switch/Case可能可以搞定。但是當狀態數目很多的時候(實際系統中也正是如此),維護一大組的Switch/Case語句將是一件異常困難並且容易出錯的事情。
2)狀態邏輯和動作實現沒有分離。在很多的系統實現中,動作的實現代碼直接寫在狀態的邏輯當中。這帶來的後果就是系統的擴展性和維護得不到保證。
State模式就是被用來解決上面列出的兩個問題的,在State模式中我們將狀態邏輯和動作實現進行分離。當一個操作中要維護大量的case分支語句,並且這些分支依賴於對象的狀態。State模式將每一個分支都封裝到獨立的類中。State模式典型的結構圖爲:
State模式在實現中,有兩個關鍵點:
1)將State聲明爲Context的友元類(friend class),其作用是讓State模式訪問Context的protected接口ChangeSate()。
2)State及其子類中的操作都將Context*傳入作爲參數,其主要目的是State類可以通過這個指針調用Context中的方法(在本示例代碼中沒有體現)。這也是State模式和Strategy模式的最大區別所在。
運行了示例代碼後可以獲得以下的結果:連續3次調用了Context的OprationInterface()因爲每次調用後狀態都會改變(A-B-A),因此該動作隨着Context的狀態的轉變而獲得了不同的結果。
State模式的應用也非常廣泛,從最高層邏輯用戶接口GUI到最底層的通訊協議(例如GoF在《設計模式》中就利用State模式模擬實現一個TCP連接的類。)都有其用武之地。
State模式和Strategy模式又很大程度上的相似:它們都有一個Context類,都是通過委託(組合)給一個具有多個派生類的多態基類實現Context的算法邏輯。兩者最大的差別就是State模式中派生類持有指向Context對象的引用,並通過這個引用調用Context中的方法,但在Strategy模式中就沒有這種情況。因此可以說一個State實例同樣是Strategy模式的一個實例,反之卻不成立。實際上State模式和Strategy模式的區別還在於它們所關注的點不盡相同:State模式主要是要適應對象對於狀態改變時的不同處理策略的實現,而Strategy則主要是具體算法和實現接口的解耦(coupling),Strategy模式中並沒有狀態的概念(雖然很多時候有可以被看作是狀態的概念),並且更加不關心狀態的改變了。
State模式很好地實現了對象的狀態邏輯和動作實現的分離,狀態邏輯分佈在State的派生類中實現,而動作實現則可以放在Context類中實現(這也是爲什麼State派生類需要擁有一個指向Context的指針)。這使得兩者的變化相互獨立,改變State的狀態邏輯可以很容易複用Context的動作,也可以在不影響State派生類的前提下創建Context的子類來更改或替換動作實現。
State模式問題主要是邏輯分散化,狀態邏輯分佈到了很多的State的子類中,很難看到整個的狀態邏輯圖,這也帶來了代碼的維護問題。
3.4 Observer模式
Observer模式應該可以說是應用最多、影響最廣的模式之一,因爲Observer的一個實例Model/View/Control(MVC)結構在系統開發架構設計中有着很重要的地位和意義,MVC實現了業務邏輯和表示層的解耦。個人也認爲Observer模式是軟件開發過程中必須要掌握和使用的模式之一。在MFC中,Doc/View(文檔視圖結構)提供了實現MVC的框架結構(有一個從設計模式(Observer模式)的角度分析分析Doc/View的文章正在進一步的撰寫當中,遺憾的是時間:))。在Java陣容中,Struts則提供和MFC中Doc/View結構類似的實現MVC的框架。另外Java語言本身就提供了Observer模式的實現接口,這將在討論中給出。
當然,MVC只是Observer模式的一個實例。Observer模式要解決的問題爲:建立一個一(Subject)對多(Observer)的依賴關係,並且做到當“一”變化的時候,依賴這個“一”的多也能夠同步改變。最常見的一個例子就是:對同一組數據進行統計分析時候,我們希望能夠提供多種形式的表示(例如以表格進行統計顯示、柱狀圖統計顯示、百分比統計顯示等)。這些表示都依賴於同一組數據,我們當然需要當數據改變的時候,所有的統計的顯示都能夠同時改變。Observer模式就是解決了這一個問題。
這裏的目標Subject提供依賴於它的觀察者Observer的註冊(Attach)和註銷(Detach)操作,並且提供了使得依賴於它的所有觀察者同步的操作(Notify)。觀察者Observer則提供一個Update操作,注意這裏的Observer的Update操作並不在Observer改變了Subject目標狀態的時候就對自己進行更新,這個更新操作要延遲到Subject對象發出Notify通知所有Observer進行修改(調用Update)。
在Observer模式的實現中,Subject維護一個list作爲存儲其所有觀察者的容器。每當
調用Notify操作就遍歷list中的Observer對象,並廣播通知改變狀態(調用Observer的Update操作)。目標的狀態state可以由Subject自己改變(示例),也可以由Observer的某個操作引起state的改變(可調用Subject的SetState操作)。Notify操作可以由Subject目標主動廣播(示例),也可以由Observer觀察者來調用(因爲Observer維護一個指向Subject的指針)。
運行示例程序,可以看到當Subject處於狀態“old”時候,依賴於它的兩個觀察者都顯示“old”,當目標狀態改變爲“new”的時候,依賴於它的兩個觀察者也都改變爲“new”Observer是影響極爲深遠的模式之一,也是在大型系統開發過程中要用到的模式之一。除了MFC、Struts提供了MVC的實現框架,在Java語言中還提供了專門的接口實現Observer模式:通過專門的類Observable及Observer接口來實現MVC編程模式,其UML圖可以表示爲:
這裏的Observer就是觀察者,Observable則充當目標Subject的角色。
Observer模式也稱爲發佈-訂閱(publish-subscribe),目標就是通知的發佈者,觀察者則是通知的訂閱者(接受通知)。
3.5 Memento模式
沒有人想犯錯誤,但是沒有人能夠不犯錯誤。犯了錯誤一般只能改過,卻很難改正(恢復)。世界上沒有後悔藥,但是我們在進行軟件系統的設計時候是要給用戶後悔的權利(實際上可能也是用戶要求的權利:)),我們對一些關鍵性的操作肯定需要提供諸如撤銷(Undo)的操作。那這個後悔藥就是Memento模式提供的。
Memento模式的關鍵就是要在不破壞封裝行的前提下,捕獲並保存一個類的內部狀態,這樣就可以利用該保存的狀態實施恢復操作。爲了達到這個目標,可以在後面的實現中看到我們採取了一定語言支持的技術。Memento模式的典型結構圖爲:
Memento模式的關鍵就是friend class Originator;我們可以看到,Memento的接口都聲明爲private,而將Originator聲明爲Memento的友元類。我們將Originator的狀態保存在Memento類中,而將Memento接口private起來,也就達到了封裝的功效。
在Originator類中我們提供了方法讓用戶後悔:RestoreToMemento(Memento* mt);我們可以通過這個接口讓用戶後悔。在測試程序中,我們演示了這一點:Originator的狀態由old變爲new最後又回到了old。
討論
在Command模式中,Memento模式經常被用來維護可以撤銷(Undo)操作的狀態。這一點將在Command模式具體說明。
3.6 Mediator模式
在面向對象系統的設計和開發過程中,對象之間的交互和通信是最爲常見的情況,因爲對象間的交互本身就是一種通信。在系統比較小的時候,可能對象間的通信不是很多、對象也比較少,我們可以直接硬編碼到各個對象的方法中。但是當系統規模變大,對象的量變引起系統複雜度的急劇增加,對象間的通信也變得越來越複雜,這時候我們就要提供一個專門處理對象間交互和通信的類,這個中介者就是Mediator模式。Mediator模式提供將對象間的交互和通訊封裝在一個類中,各個對象間的通信不必顯勢去聲明和引用,大大降低了系統的複雜性能(瞭解一個對象總比深入熟悉n個對象要好)。另外Mediator模式還帶來了系統對象間的鬆耦合,這些將在討論中詳細給出。
Mediator模式典型的結構圖爲:
Mediator模式中,每個Colleague維護一個Mediator,當要進行交互,例如圖中ConcreteColleagueA和ConcreteColleagueB之間的交互就可以通過ConcreteMediator提供的DoActionFromAtoB來處理,ConcreteColleagueA和ConcreteColleagueB不必維護對各自的引用,甚至它們也不知道各個的存在。Mediator通過這種方式將多對多的通信簡化爲了一(Mediator)對多(Colleague)的通信。
Mediator模式的實現關鍵就是將對象Colleague之間的通信封裝到一個類種單獨處理,爲了模擬Mediator模式的功能,這裏給每個Colleague對象一個string型別以記錄其狀態,並通過狀態改變來演示對象之間的交互和通信。這裏主要就Mediator的示例運行結果給出分析:
1)將ConcreteColleageA對象設置狀態“old”,ConcreteColleageB也設置狀態“old”;
2)ConcreteColleageA對象改變狀態,並在Action中和ConcreteColleageB對象進行通信,並改變ConcreteColleageB對象的狀態爲“new”;
3)ConcreteColleageB對象改變狀態,並在Action中和ConcreteColleageA對象進行通信,並改變ConcreteColleageA對象的狀態爲“new”;
注意到,兩個Colleague對象並不知道它交互的對象,並且也不是顯示地處理交互過程,這一切都是通過Mediator對象完成的,示例程序運行的結果也正是證明了這一點。
Mediator模式是一種很有用並且很常用的模式,它通過將對象間的通信封裝到一個類中,將多對多的通信轉化爲一對多的通信,降低了系統的複雜性。Mediator還獲得系統解耦的特性,通過Mediator,各個Colleague就不必維護各自通信的對象和通信協議,降低了系統的耦合性,Mediator和各個Colleague就可以相互獨立地修改了。
Mediator模式還有一個很顯著額特點就是將控制集中,集中的優點就是便於管理,也正式符合了OO設計中的每個類的職責要單一和集中的原則。
3.7 Command模式
Command模式通過將請求封裝到一個對象(Command)中,並將請求的接受者存放到具體的ConcreteCommand類中(Receiver)中,從而實現調用操作的對象和操作的具體實現者之間的解耦。
Command模式結構圖中,將請求的接收者(處理者)放到Command的具體子類ConcreteCommand中,當請求到來時(Invoker發出Invoke消息激活Command對象),ConcreteCommand將處理請求交給Receiver對象進行處理。
Command模式在實現的實現和思想都很簡單,其關鍵就是將一個請求封裝到一個類中(Command),再提供處理對象(Receiver),最後Command命令由Invoker激活。另外,我們可以將請求接收者的處理抽象出來作爲參數傳給Command對象,實際也就是回調的機制(Callback)來實現這一點,也就是說將處理操作方法地址(在對象內部)通過參數傳遞給Command對象,Command對象在適當的時候(Invoke激活的時候)再調用該函數。這裏就要用到C++中的類成員函數指針的概念,爲了方便學習,這裏給出一個簡單的實現源代碼供參考:
Command模式的思想非常簡單,但是Command模式也十分常見,並且威力不小。實際上,Command模式關鍵就是提供一個抽象的Command類,並將執行操作封裝到Command類接口中,Command類中一般就是隻是一些接口的集合,並不包含任何的數據屬性(當然在示例代碼中,我們的Command類有一個處理操作的Receiver類的引用,但是其作用也僅僅就是爲了實現這個Command的Excute接口)。這種方式在是純正的面向對象設計者最爲鄙視的設計方式,就像OO設計新手做系統設計的時候,僅僅將Class作爲一個關鍵字,將C種的全局函數找一個類封裝起來就以爲是完成了面向對象的設計。
但是世界上的事情不是絕對的,上面提到的方式在OO設計種絕大部分的時候可能是一個不成熟的體現,但是在Command模式中卻是起到了很好的效果。主要體現在:
1) Command模式將調用操作的對象和知道如何實現該操作的對象解耦。在上面Command的結構圖中,Invoker對象根本就不知道具體的是那個對象在處理Excute操作(當然要知道是Command類別的對象,也僅此而已)。
2) 在Command要增加新的處理操作對象很容易,我們可以通過創建新的繼承自Command的子類來實現這一點。
3) Command模式可以和Memento模式結合起來,支持取消的操作。
3.8 Visitor模式
在面向對象系統的開發和設計過程,經常會遇到一種情況就是需求變更(Requirement Changing),經常我們做好的一個設計、實現了一個系統原型,咱們的客戶又會有了新的需求。我們又因此不得不去修改已有的設計,最常見就是解決方案就是給已經設計、實現好的類添加新的方法去實現客戶新的需求,這樣就陷入了設計變更的夢魘:不停地打補丁,其帶來的後果就是設計根本就不可能封閉、編譯永遠都是整個系統代碼。
Visitor模式則提供了一種解決方案:將更新(變更)封裝到一個類中(訪問操作),並由待更改類提供一個接收接口,則可達到效果。
Visitor模式在不破壞類的前提下,爲類提供增加新的新操作。Visitor模式的關鍵是雙分派(Double-Dispatch)的技術【註釋1】。C++語言支持的是單分派。
在Visitor模式中Accept()操作是一個雙分派的操作。具體調用哪一個具體的Accept()操作,有兩個決定因素:1)Element的類型。因爲Accept()是多態的操作,需要具體的Element類型的子類纔可以決定到底調用哪一個Accept()實現;2)Visitor的類型。Accept()操作有一個參數(Visitor* vis),要決定了實際傳進來的Visitor的實際類別纔可以決定具體是調用哪個VisitConcrete()實現。
Visitor模式的實現過程中有以下的地方要注意:
1)Visitor類中的Visit()操作的實現。
􀂋 這裏我們可以向Element類僅僅提供一個接口Visit(),而在Accept()實現中具體調用哪一個Visit()操作則通過函數重載(overload)的方式實現:我們提供Visit()的兩個重載版本a)Visit(ConcreteElementA* elmA),b)Visit(ConcreteElementB* elmB)。
􀂋 在C++中我們還可以通過RTTI(運行時類型識別:Runtime type identification)來實現,即我們只提供一個Visit()函數體,傳入的參數爲Element*型別參數 ,然後用RTTI決定具體是哪一類的ConcreteElement參數,再決定具體要對哪個具體類施加什麼樣的具體操作。【註釋2】RTTI給接口帶來了簡單一致性,但是付出的代價是時間(RTTI的實現)和代碼的Hard編碼(要進行強制轉換)。
有時候我們需要爲Element提供更多的修改,這樣我們就可以通過爲Element提供一系列的
Visitor模式可以使得Element在不修改自己的同時增加新的操作,但是這也帶來了至少以下的兩個顯著問題:
1) 破壞了封裝性。Visitor模式要求Visitor可以從外部修改Element對象的狀態,這一般通過兩個方式來實現:a)Element提供足夠的public接口,使得Visitor可以通過調用這些接口達到修改Element狀態的目的;b)Element暴露更多的細節給Visitor,或者讓Element提供public的實現給Visitor(當然也給了系統中其他的對象),或者將Visitor聲明爲Element的friend類,僅將細節暴露給Visitor。但是無論那種情況,特別是後者都將是破壞了封裝性原則(實際上就是C++的friend機制得到了很多的面向對象專家的詬病)。
2) ConcreteElement的擴展很困難:每增加一個Element的子類,就要修改Visitor的
接口,使得可以提供給這個新增加的子類的訪問機制。從上面我們可以看到,或者增加一個用於處理新增類的Visit()接口,或者重載一個處理新增類的Visit()操作,或者要修改RTTI方式實現的Visit()實現。無論那種方式都給擴展新的Element子類帶來了困難。
3.9 Chain of Responsibility模式
熟悉VC/MFC的都知道,VC是“基於消息,事件驅動”,消息在VC開發中起着舉足輕重的作用。在MFC中,消息是通過一個向上遞交的方式進行處理,例如一個WM_COMMAND消息的處理流程可能爲:
1) MDI主窗口(CMDIFrameWnd)收到命令消息WM_COMMAND,其ID位ID_×××;
2) MDI主窗口將消息傳給當前活動的MDI子窗口(CMDIChildWnd);
3) MDI子窗口給自己的子窗口(View)一個處理機會,將消息交給View;
4) View檢查自己Message Map;
5) 如果View沒有發現處理該消息的程序,則將該消息傳給其對應的Document對象;否則View處理,消息流程結束。
6) Document檢查自己Message Map,如果沒有該消息的處理程序,則將該消息傳給其對象的DocumentTemplate處理;否則自己處理,消息流程結束;
7) 如果在6)中消息沒有得到處理,則將消息返回給View;
8) View再傳回給MDI子窗口;
9) MDI子窗口將該消息傳給CwinApp對象,CwinApp爲所有無主的消息提供了處理。
註明:有關MFC消息處理更加詳細信息,請參考候捷先生的《深入淺出MFC》。
MFC提供了消息的處理的鏈式處理策略,處理消息的請求將沿着預先定義好的路徑依
次進行處理。消息的發送者並不知道該消息最後是由那個具體對象處理的,當然它也無須也不想知道,但是結構是該消息被某個對象處理了,或者一直到一個終極的對象進行處理了。
Chain of Responsibility模式描述其實就是這樣一類問題將可能處理一個請求的對象鏈接成一個鏈,並將請求在這個鏈上傳遞,直到有對象處理該請求(可能需要提供一個默認處理所有請求的類,例如MFC中的CwinApp類)。
Chain of Responsibility模式中ConcreteHandler將自己的後繼對象(向下傳遞消息的對象)記錄在自己的後繼表中,當一個請求到來時,ConcreteHandler會先檢查看自己有沒有匹配的處理程序,如果有就自己處理,否則傳遞給它的後繼。當然這裏示例程序中爲了簡化,ConcreteHandler只是簡單的檢查看自己有沒有後繼,有的話將請求傳遞給後繼進行處理,沒有的話就自己處理。
Chain of Responsibility模式的示例代碼實現很簡單,這裏就其測試結果給出說明:
ConcreteHandleA的對象和h1擁有一個後繼ConcreteHandleB的對象h2,當一個請求到來時候,h1檢查看自己有後繼,於是h1直接將請求傳遞給其後繼h2進行處理,h2因爲沒有後繼,當請求到來時候,就只有自己提供響應了。於是程序的輸出爲:
1) ConcreteHandleA 我把處理權給後繼節點.....;
2) ConcreteHandleB 沒有後繼了,我必須自己處理....。
 討論
Chain of Responsibility模式的最大的一個有點就是給系統降低了耦合性,請求的發送者完全不必知道該請求會被哪個應答對象處理,極大地降低了系統的耦合性。
3.10 Iterator模式
Iterator模式應該是最爲熟悉的模式了,最簡單的證明就是我在實現Composite模式、Flyweight模式、Observer模式中就直接用到了STL提供的Iterator來遍歷Vector或者List數據結構。
Iterator模式也正是用來解決對一個聚合對象的遍歷問題,將對聚合的遍歷封裝到一個類中進行,這樣就避免了暴露這個聚合對象的內部表示的可能。
Iterator模式中定義的對外接口可以視客戶成員的便捷定義,但是基本的接口在圖中的Iterator中已經給出了(參考STL的Iterator就知道了)。
Iterator模式的實現代碼很簡單,實際上爲了更好地保護Aggregate的狀態,我們可以儘量減小Aggregate的public接口,而通過將Iterator對象聲明位Aggregate的友元來給予Iterator一些特權,獲得訪問Aggregate私有數據和方法的機會。
􀂄 討論
Iterator模式的應用很常見,我們在開發中就經常會用到STL中預定義好的Iterator來對STL類進行遍歷(Vector、Set等)。
3.11 Interpreter模式
一些應用提供了內建(Build-In)的腳本或者宏語言來讓用戶可以定義他們能夠在系統中進行的操作。Interpreter模式的目的就是使用一個解釋器爲用戶提供一個一門定義語言的語法表示的解釋器,然後通過這個解釋器來解釋語言中的句子。
Interpreter模式提供了這樣的一個實現語法解釋器的框架,筆者曾經也正在構建一個編譯系統Visual CMCS,現在已經發布了Visual CMCS1.0 (Beta),請大家訪問Visual CMCS網站獲取詳細信息。
Interpreter模式中,提供了TerminalExpression和NonterminalExpression兩種表達式的解釋方式,Context類用於爲解釋過程提供一些附加的信息(例如全局的信息)。
XML格式的數據解析是一個在應用開發中很常見並且有時候是很難處理的事情,雖然目前很多的開發平臺、語言都提供了對XML格式數據的解析,但是例如到了移動終端設備上,由於處理速度、計算能力、存儲容量的原因解析XML格式的數據卻是很複雜的一件事情,最近也提出了很多的移動設備的XML格式解析器,但是總體上在項目開發時候還是需要自己去設計和實現這一個過程(筆者就有過這個方面的痛苦經歷)。
Interpreter模式則提供了一種很好的組織和設計這種解析器的架構。
Interpreter模式中使用類來表示文法規則,因此可以很容易實現文法的擴展。另外對於終結符我們可以使用Flyweight模式來實現終結符的共享。
4  說明

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章