亂砍設計模式之四

COMPOSITE與BUILDER模式 —— 忠義堂石碣受天文 梁山泊英雄排座次

junguo

     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, 水滸傳 —— 網上找到的電子檔
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章