Composite模式的中文名字是組合模式,該模式的目的是使單個對象和它的對象組合(一般是數組或者鏈表的結構)擁有統一的操作方式,這樣可以簡化客戶的使用。我們還是通過具體的例子來理解該模式。還是先來一段例子背景介紹:
話說,宋江帶人攻陷東平東昌兩郡,收降了雙槍將董平和沒羽箭張清,而後皇甫端來歸。梁山聚集了武將,書生,醫生以至獸醫等各色人等,成爲了一個成熟的社區。着點人數正好一百單八人,是個吉利數字,爲此宋江組織了浩大的祭天儀式。在祭天儀式進行期間,發現了埋於地下的石碣,石碣上排定了三十六天罡,七十二地煞的座次。次序以定,梁山也開始走上正規,開始劃分各位好漢的職責。依據各人特長,分別任職於馬軍,步軍,軍師等角色。有了規矩,才成方圓,自此梁山告別了單挑的時代,作戰有了一些固定的組合。如李逵出戰,必有項充,李袞護在身邊;這位頭腦簡單的殺人魔王的典型特點是攻強守弱,而且不着盔甲,如若無人保護估摸早死了。還有另外一些組合:如矮腳虎王英和一丈青扈三娘這樣的夫妻組合(扈三娘嫁給了王英,你能想通嗎?我想不通);還有解寶解珍這樣的兄弟組合等。
來考慮一下,如何實現這樣的功能,武將可以單獨出擊,也可以成羣的出擊?該如何去實現這樣的功能呢?還是一步一步來,先看類圖:
這是我們建立的武將類,該類和我們第一講《趙子龍單騎救主》中建立的武將類相同(爲了突出中心功能,簡化了功能)。考慮一下在這種情況下,我們如果想讓多個武將一起出擊,該如何實現?最單純的做法就是在調用該類的地方生成一個數組,然後客戶端通過該數組來調用各個對象的操作。大概代碼如下:
General general[10]; for( int i = 0; i<10; i++ ) general[i].Assault();
客戶端也可以將這些操作封裝在不同的函數中,但如果武將類填加新的函數的時候,客戶端也需要相應的增加此類德函數。這是典型的封裝不好的例子,客戶端需要增加特殊的函數來支持對象。我自己的工作中就遇到了這樣的問題,類沒有封裝好,針對於這些類的操作,你不得不填加一些函數,接着就可以發現在不同的文件裏有很多功能相似的函數(一個類往往在多處用到,而這些往往是由不同的人寫的,這樣很容易造成重複代碼)。整個程序被改得亂碼七糟的,改這樣的東西真是頭痛萬分,更加上你如果要用到這些外部的函數的時候,都不知道用那個。那如何來解決該問題呢?找到組合模式,就找到了解決之道。先來看看組合模式的類圖描述:
Component是爲Leaf和Composite抽象出來的共同接口,可以通過它來統一操作單個對象或者組合對象。
Leaf是我們具體要用到的類,它用來完成具體的功能。Composite是我們用來組合對象的類,並實現對子對象的管理;它通過Add,Remove和GetChild來實現對子對象的管理。Composite中會含有一個Component型的對象列表childers,它也會重載Leaf中所需要的函數,然後通過遍歷執行所有childer的函數。該childer也可以是Composite型的,這樣就可以有一個遞歸的結構。
我們用Composite模式來重新設計我們的類圖,如下:
理解模式最有效的方式,還是看具體的代碼:
#include #include #include #include using namespace std; //抽象部件類,是Leaf和Composite的共有接口 class GeneralComponent { public: //爲了確保調用到析構函數,此處聲明爲虛函數 virtual ~GeneralComponent(){} //由於General中不實現Add,Remove,GetChild,此處需要一個缺省的實現,但此處拋出異常更合適一些,爲了簡單,省略了。 virtual void Add(GeneralComponent* pComonent) {} virtual void Remove(GeneralComponent* pComonent) {} virtual GeneralComponent* GetChild(int i) {return NULL;} virtual void Assault() = 0; }; //具體用到的類 class General : public GeneralComponent { private: string m_strName; public: General(string strName):m_strName(strName){} void Assault() { cout << m_strName << " 進入戰鬥!" << endl; } }; //組合類 class GeneralComposite : public GeneralComponent { private: string m_strName; //用來存放需要組合的對象 vector m_pComponents; public: GeneralComposite(string strName):m_strName(strName){} virtual ~GeneralComposite() { vector::iterator pos; for( pos = m_pComponents.begin();pos::iterator ivite= find(m_pComponents.begin(),m_pComponents.end(),pGeneral); m_pComponents.erase(ivite); } GeneralComponent* GetChild(int i) { return m_pComponents[i]; } void Assault() { cout << m_strName << "戰鬥序列" << endl; //需要調用所有的組合對象的操作 vector::iterator pos; for( pos = m_pComponents.begin();posAssault(); } } };
我們再來看一下,客戶端如何來調用該對象:
int main(int argc, char* argv[]) { GeneralComposite pArmy("梁山大軍"); GeneralComposite* pHorseArmy = new GeneralComposite("梁山馬軍"); GeneralComposite* pPaceArmy = new GeneralComposite("梁山步軍"); General *pWusong = new General("好漢武松"); General *pHuaheshang = new General("俠客魯智深"); General *pLida = new General("莽漢李逵"); General *pLinchong = new General("英雄林沖"); General *pGuansheng = new General("大刀關勝"); pHorseArmy->Add(pLinchong); pHorseArmy->Add(pGuansheng); pPaceArmy->Add(pWusong); pPaceArmy->Add(pHuaheshang); pPaceArmy->Add(pLida); pArmy.Add(pHorseArmy); pArmy.Add(pPaceArmy); pArmy.Assault(); return 0; }
運行結果如下:
我們可以看到,通過組合模式,對於對象的調用變得簡單了起來,只要通過一個函數就可以實現對所有對象的統一調用。這樣做最大的好處就是將所有的代碼統一成了一份,可以避免在不同的地方出現類似的代碼。在多人合作的情況下,由於交流不充分的問題,很多時候開發人員各自寫各自的代碼,而不關心別人的代碼,極容易產生相同功能的代碼。當我們的類進行擴展的時候,可能很多地方都需要進行修改。
我們再來看看組合模式可能存在的問題。Leaf類繼承了Component中的所有方法,但對於Add,Remove,GetChild等操作,這些操作對它來說是無意義的。我們也可以把這些操作放到Composite中,但在這種情況下,我們將無法通過基類來直接操作Composite對象。個人覺得這個問題可以根據具體問題權衡解決。我們也可以爲Component擴展更多的Leaf或者Composite,在這種情況下,如果我們想控制Composite中只能添加限定的Leaf或者Composite在靜態情況下是不可能的,必須在運行時刻去判斷。
還有,我們應該最大化Component接口,我們使用該接口的目的就是使用戶不知道它具體操作的是哪個Leaf或者Composite,所以我們應該儘量多定義一些它們公共的操作。這樣還是會造成我們上面提到的會給一些類帶來無意義的功能。
我們再來簡單比較一下Composite和Decorator模式,這兩個模式都會有一個子類擁有基類的指針,不同之處是Composite會擁有多個組件,而Decorator只擁有一個組件。Composite主要目的是對象的聚集,使它們的調用統一化;而Decorator不是此目的,它的目的是給對象添加一些額外的功能。
好了,Composite的講解先到此爲止。如果你感興趣的話,可以試着把我們第一章用到的策略模式應用到該處。不過我們下面看另外一個問題。在main中,我們需要很多代碼去完成對象的創建。我們是否可以將對象的創建部分也做一些封裝呢?我們可以找找創建型的模式中是否有適合我們的模式?
在《設計模式》中,我們可以找到Builder模式,它的定義是:將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。和我們的意圖有點象,先來看看《設計模式》中爲我們提供的類圖:
作一下解釋:
1, Director是具體要用到複雜對象的部分(如GeneralComponent)的類(我們程序中是main中調用)。它會通過Builder來建造自己需要的對象,而不需要自己通過new來得到所要的對象。
2, 而Builder的是一個抽象的創建對象的接口。
3, ConcreteBuilder是我們用來具體創建對象的類。它可能有多個,用來幫我們建造不同類型的對象。如我們把我們的General派生出不同的風格,如先秦風格和羅馬風格的時候,我們可能需要生成不同的類。而每次創建一種風格的對象,我們就可以擴展Builder而生成不同的創建類。
我們的例子比較簡單,不需要這麼多東西。Builder的重要特徵是:它是用來一步一步創建複雜對象類型的。我們的組合類由於需要組合不同的組件,所以整個創建過程比較複雜,那麼可以借用Builder模式的特點來創建我們的對象。好的,看看具體的代碼:
class Builder { public: static GeneralComponent* CreateCompise(GeneralComponent *pParent,string strArr[],int iLength = 1) { if ( pParent != NULL ) { for( int i = 0 ; i < iLength ; i++ ) { pParent->Add(new GeneralComposite(strArr[i])); } return pParent; } else { return new GeneralComposite(strArr[0]); } } static GeneralComponent* CreateGeneral(GeneralComponent *pParent,string strArr[],int iLength = 1) { if ( pParent != NULL ) { for( int i = 0 ; i < iLength ; i++ ) { pParent->Add(new General(strArr[i])); } return pParent; } else { return new General(strArr[0]); } } };
這邊的代碼略顯簡單,只重意了,你可以看到,創建過程被封裝成CreateCompise和CreateGeneral兩個方法,通過它們來創建我們所需要的對象。只要將所需要的Composite(爲它創建Leaf和子Composite的),還有創建對象所需要的參數(例子有些簡單,只要一個初始化參數)數組和數組長度傳進函數就可以幫我們創建所需要的對象了。由於Builder沒有成員變量,將兩個函數設置成了靜態函數,想當於一個單件類型。我們再看看它的具體用法:
int main(int argc, char* argv[]) { string strArmy[1] = {"梁山大軍"}; string strArmyChild[2] = {"梁山馬軍","梁山步軍"}; string strSpecLeader[3] = {"好漢武松","俠客魯智深","莽漢李逵"}; string strHorseLeader[2] = {"英雄林沖","大刀關勝"}; GeneralComponent *pArmy = Builder::CreateCompise(NULL,strArmy); Builder::CreateCompise(pArmy,strArmyChild,2); Builder::CreateGeneral(pArmy->GetChild(0),strHorseLeader,2); Builder::CreateGeneral(pArmy->GetChild(1),strSpecLeader,3); pArmy->Assault(); delete pArmy; }
這樣我們就可以統一對象的創建過程。做到一定程度的聚合。我個人感覺模式的學習過程是從形到意(就是從一些具體的例子看起,通過具體的例子來理解模式);而後是重意不重形(明白模式的意義之後,我們就可以隨意使用模式了,而不需要硬套公式)。所以此處使用了和書上並不一致的Builder模型。其實Builder模式與Abstract Factory模式很相似,我們下次描述Abstract Factory模式的時候,再回頭比較一下。
參考書目: 1, 設計模式——可複用面向對象軟件的基礎(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英軍等譯 機械工業出版社 2, Head First Design Patterns(影印版)Freeman等著 東南大學出版社 3, 道法自然——面向對象實踐指南 王詠武 王詠剛著 電子工業出版社 4, 水滸傳 —— 網上找到的電子檔