模式羅漢拳:Composite模式與自動化測試框架的實現(xiàn)
透明
梗概
Composite模式將相似的對象以樹型結(jié)構(gòu)的方式組合在一起,使開發(fā)者可以創(chuàng)建復雜的對象。另外,Composite模式要求樹中的對象都有共同的超類(或者說:接口),因此可以用同樣的方式來處理樹中的對象。
場景
講到Composite模式,總會涉及“文檔格式化”這個例子,我也繼續(xù)用這個例子。假設你正在設計一個文檔格式化程序,這個程序的作用就是把字(character)格式化為行(line of text),很多行就組織成欄(column),欄再組織成頁(page)。一篇文檔(document)還可能包括其他的元素,例如圖片(image)之類的。欄和頁中還可以有框(frame),框中可以再容納欄。欄、框和行都可以包含圖片。從上面的描述,你可以得到這樣的一個設計:

圖1:文檔結(jié)構(gòu)
這些不同的關聯(lián)使得系統(tǒng)的復雜度變得巨大。你可以想象,維護這樣的一個系統(tǒng)會有多困難!如果使用Composite模式,系統(tǒng)的復雜度會大大降低(如圖2所示)。

圖2:使用Composite模式,系統(tǒng)變得清晰
約束
- 你手上有一個復雜的對象,希望將它分解成一個“由部分組成整體”的類體系。
- 你希望盡量減少這個類體系的復雜度。為了達到這個目的,應該讓繼承樹中的每個子對象都盡量少去了解其他子對象。
解決方案
使用Composite模式。所有類都派生自共同的基類(在上面的例子中就是DocumentElement);然后,可以包容其他元素的類派生自CompositeDocumentElement類(這個類也派生自DocumentElement類)。Composite模式的一般結(jié)構(gòu)如圖3所示,具體的實現(xiàn)細節(jié)我們將在下面講到。

圖3:Composite模式的結(jié)構(gòu)
效果
- 樹型結(jié)構(gòu)的組合對象可以把其中包含的所有對象都當作AbstractComponent類的實例來處理,不管這些對象實際上是簡單對象還是組合對象。
- 客戶可以把組合對象也當作AbstractComponent類的實例來處理,而不必再去了解子類的實現(xiàn)細節(jié)。
- 如果客戶在AbstractComposite派生類的對象上調(diào)用了AbstractComponent類的方法,該對象就會把調(diào)用轉(zhuǎn)發(fā)給其中包含的AbstractComponent對象。
- 如果客戶調(diào)用方法的對象是AbstractComponent的派生對象、而不是AbstractComposite的派生對象,并且這個方法還需要與場景相關的信息,那么AbstractComponent對象就會把請求轉(zhuǎn)發(fā)給自己的父對象(也就是包容自己的對象)。
- Composite模式允許任何AbstractComponent派生對象成為任何AbstractComposite派生對象的子對象(也就是被容納的對象)。如果你需要更多的限制,就必須為AbstractComposite及其子類加上類型判別代碼,這就會使Composite模式的價值打折扣。
- 被容納的對象可能會有特有的方法。你可以在AbstractComponent類中聲明這個方法,并給它一個空的實現(xiàn),這樣就可以在組合對象中使用這個方法,而且不需要引入類型判別代碼。
實現(xiàn)
- 如果被包容的對象需要向包容對象轉(zhuǎn)發(fā)請求,那么你可以讓被包容對象保存一個指向包容對象的指針,這樣轉(zhuǎn)發(fā)會更簡單。
- 如果要讓被包容對象保存包容對象的指針,那么就必須有某種機制來保證兩者關聯(lián)的一致性。最好是在Add()方法(或其他功能相似的方法)中添加相關的設置。
- 出于效率的考慮,對象可以把父對象轉(zhuǎn)發(fā)過來的方法調(diào)用的結(jié)果暫存(cache)起來。
- 如果子對象暫存了方法調(diào)用的結(jié)果,當結(jié)果不再正確的時候,父對象就必須提醒子對象。
實現(xiàn)一個自動化測試框架
第一個問題:什么是“自動化測試框架”?顧名思義,自動化測試框架(automated testing framework)就是可以自動對代碼進行單元測試的框架。在傳統(tǒng)的軟件開發(fā)流程中,計劃、設計、編碼和測試都有各自獨立的階段,階段之間不回溯,所以測試是不是自動化并不重要——反正有的是時間來慢慢測試。但是,在新的軟件開發(fā)流程中,迭代周期變短,要求對代碼進行頻繁地重構(gòu)。而這就要求單元測試必須能夠自動、簡便、高速地運行,否則重構(gòu)就是不現(xiàn)實的[1]。
OK,我假設你已經(jīng)明白了測試框架的作用,現(xiàn)在我們來看看它的需求。別忘了,這可是“自動化”的測試框架,它應該簡單到開發(fā)者按一個按鈕就能完成所有測試的程度。所以,我們必須以某種方式將測試用例(test case)組織成一個測試套件(suite),然后才能很方便地自動運行它;此外,還必須能很簡單地向套件中添加新的測試用例,添加多少都可以,而且還不影響套件的正常運行;而且,測試套件還應該可以隨意組合,也就是說:一個套件應該可以包含其他的套件。
看看這些需求,想到了什么?很明顯,這就是一個Composite模式。簡單的結(jié)構(gòu)如圖4:

圖4:CUnit的核心框架
在《Refactoring》中,Martin Fowler介紹了Java的自動化測試框架JUnit。參考JUnit的結(jié)構(gòu),我用C++寫了一個測試框架CUnit,圖4就是CUnit的結(jié)構(gòu)。這是一個典型的Composite模式:TestSuite可以容納任何派生自Test的對象;當調(diào)用TestSuite對象的run()方法時,它會遍歷自己容納的對象,逐個調(diào)用它們的run()方法;客戶無須關心自己拿到的究竟是TestCase還是TestSuite,他(它)只管調(diào)用對象的run()方法,然后分析run()方法返回的結(jié)果就行了[2]。
代碼示例
下面,我們來看看CUnit的一些關鍵代碼。首先是Test類,它定義了一個公用的接口:
class Test
{
public:
virtual void run() = 0;
};
然后,TestCase類繼承了Test類,加入了兩個新方法setUp()和tearDown()(關于這兩個方法的用途,請參見《Refactoring》一書的相關章節(jié))。TestCase不實現(xiàn)run()方法,所以它也是一個抽象類。用戶需要從TestCase類派生出自己的測試用例類,并根據(jù)自己的需要來實現(xiàn)run()方法。另外,用戶可能想自己實現(xiàn)setUp()和tearDown()這兩個方法,也有可能不做任何實現(xiàn),所以這兩個方法應該是虛方法,但不能是純虛方法。 class TestCase : public Test
{
protected:
virtual void setUp(){ };
virtual void tearDown(){ };
};
TestSuite類也繼承了Test類。由于TestSuite是一個Composite類,所以它能夠容納其他Test類型的對象(用addTest()方法添加);而TestSuite::run()則遍歷這些被包容的對象,逐個調(diào)用它們的run()方法。 class TestSuite : public Test
{
public:
void addTest(Test * test){
m_Compositee.push_back(test);
};
virtual void run(){
for(int i=0; irun();
};
private:
vector m_Compositee;
};
以上就是CUnit的主要代碼。當然,要實現(xiàn)自動化的單元測試,僅靠這個類體系是遠遠不夠的,還需要其他很多的技巧。我把CUnit的全部代碼上傳到了http://gigix.topcool.net/download/03.zip,歡迎有興趣的讀者與我一起討論。
相關模式
- Chain of Responsibility模式
添加相應的父對象連接,就可以把Chain of Responsibility模式和Composite模式組合起來。這樣,子對象就可以從某個祖先那里得到信息,而不必知道究竟從哪個祖先得到信息。
- Visitor模式
Visitor模式可以把Composite模式中散布在多個類中的操作封裝在一個類中。
注釋
[1] 關于重構(gòu)和自動化測試框架的概念,參見Martin Fowler的《Refactoring》。此外,《程序員》雜志2001年第12期技術專題“代碼重構(gòu)”也有關于重構(gòu)的知識。
[2] 實際上我的CUnit與JUnit還有一定的差異,因為我的主要目的是為了闡述Composite模式的應用,而非設計真正實用的測試框架。在Source Forge上有一個叫CppUnit的項目,其結(jié)構(gòu)與JUnit幾乎毫無二致,而且也更加完善。