一、
引子
在大學(xué)的數(shù)據(jù)結(jié)構(gòu)這門(mén)課上,樹(shù)是最重要的章節(jié)之一。還記得樹(shù)是怎么定義的嗎?樹(shù)
(Tree)
是
n(n≥0)
個(gè)結(jié)點(diǎn)的有限集
T
,
T
為空時(shí)稱(chēng)為空樹(shù),否則它滿(mǎn)足如下兩個(gè)條件:
(1)???
有且僅有一個(gè)特定的稱(chēng)為根
(Root)
的結(jié)點(diǎn);
(2)??
其余的結(jié)點(diǎn)可分為
m(m≥0)
個(gè)互不相交的子集
Tl
,
T2
,
…
,
Tm
,其中每個(gè)子集本身又是一棵樹(shù),并稱(chēng)其為根的子樹(shù)
(SubTree)
。
上面給出的遞歸定義刻畫(huà)了樹(shù)的固有特性:一棵非空樹(shù)是由若干棵子樹(shù)構(gòu)成的,而子樹(shù)又可由若干棵更小的子樹(shù)構(gòu)成。而這里的子樹(shù)可以是葉子也可以是分支。
今天要學(xué)習(xí)的組合模式就是和樹(shù)型結(jié)構(gòu)以及遞歸有關(guān)系。
?
二、
定義與結(jié)構(gòu)
組合
(Composite)
模式的其它翻譯名稱(chēng)也很多,比如合成模式、樹(shù)模式等等。在《設(shè)計(jì)模式》一書(shū)中給出的定義是:將對(duì)象以樹(shù)形結(jié)構(gòu)組織起來(lái),以達(dá)成
“
部分-整體
”
的層次結(jié)構(gòu),使得客戶(hù)端對(duì)單個(gè)對(duì)象和組合對(duì)象的使用具有一致性。
從定義中可以得到使用組合模式的環(huán)境為:
在設(shè)計(jì)中想表示對(duì)象的
“
部分
-
整體
”
層次結(jié)構(gòu);希望用戶(hù)忽略組合對(duì)象與單個(gè)對(duì)象的不同,統(tǒng)一地使用組合結(jié)構(gòu)中的所有對(duì)象。
看下組合模式的組成。
1)????????
抽象構(gòu)件角色
Component
:它為組合中的對(duì)象聲明接口,也可以為共有接口實(shí)現(xiàn)缺省行為。
2)??????
樹(shù)葉構(gòu)件角色
Leaf
:在組合中表示葉節(jié)點(diǎn)對(duì)象
——
沒(méi)有子節(jié)點(diǎn),實(shí)現(xiàn)抽象構(gòu)件角色聲明的接口。
3)??????
樹(shù)枝構(gòu)件角色
Composite
:在組合中表示分支節(jié)點(diǎn)對(duì)象
——
有子節(jié)點(diǎn),實(shí)現(xiàn)抽象構(gòu)件角色聲明的接口;存儲(chǔ)子部件。
下圖為組合模式的類(lèi)圖表示。
?
如圖所示:一個(gè)
Composite
實(shí)例可以像一個(gè)簡(jiǎn)單的
Leaf
實(shí)例一樣,可以把它傳遞給任何使用
Component
的方法或者對(duì)象,并且它表現(xiàn)的就像是一個(gè)
Leaf
一樣。
可以看出來(lái),使用組合模式使得這個(gè)設(shè)計(jì)結(jié)構(gòu)非常靈活,在下面的例子中會(huì)得到進(jìn)一步的印證。
??????
三、
安全性與透明性
組合模式中必須提供對(duì)子對(duì)象的管理方法,不然無(wú)法完成對(duì)子對(duì)象的添加刪除等等操作,也就失去了靈活性和擴(kuò)展性。但是管理方法是在
Component
中就聲明還是在
Composite
中聲明呢?
一種方式是在
Component
里面聲明所有的用來(lái)管理子類(lèi)對(duì)象的方法,以達(dá)到
Component
接口的最大化(如下圖所示)。目的就是為了使客戶(hù)看來(lái)在接口層次上樹(shù)葉和分支沒(méi)有區(qū)別
——
透明性。但樹(shù)葉是不存在子類(lèi)的,因此
Component
聲明的一些方法對(duì)于樹(shù)葉來(lái)說(shuō)是不適用的。這樣也就帶來(lái)了一些安全性問(wèn)題。
?
另一種方式就是只在
Composite
里面聲明所有的用來(lái)管理子類(lèi)對(duì)象的方法(如下圖所示)。這樣就避免了上一種方式的安全性問(wèn)題,但是由于葉子和分支有不同的接口,所以又失去了透明性。
????
????《設(shè)計(jì)模式》一書(shū)認(rèn)為:在這一模式中,相對(duì)于安全性,我們比較強(qiáng)調(diào)透明性。對(duì)于第一種方式中葉子節(jié)點(diǎn)內(nèi)不需要的方法可以使用空處理或者異常報(bào)告的方式來(lái)解決。
?
四、
舉例
這里以
JUnit
中的組合模式的應(yīng)用為例(JUnit入門(mén))。
JUnit
是一個(gè)單元測(cè)試框架,按照此框架下的規(guī)范來(lái)編寫(xiě)測(cè)試代碼,就可以使單元測(cè)試自動(dòng)化。為了達(dá)到“自動(dòng)化”的目的,
JUnit
中定義了兩個(gè)概念:
TestCase
和
TestSuite
。
TestCase
是對(duì)一個(gè)類(lèi)或者
jsp
等等編寫(xiě)的測(cè)試類(lèi);而
TestSuite
是一個(gè)不同
TestCase
的集合,當(dāng)然這個(gè)集合里面也可以包含
TestSuite
元素,這樣運(yùn)行一個(gè)
TestSuite
會(huì)將其包含的
TestCase
全部運(yùn)行。
然而在真實(shí)運(yùn)行測(cè)試程序的時(shí)候,是不需要關(guān)心這個(gè)類(lèi)是
TestCase
還是
TestSuite
,我們只關(guān)心測(cè)試運(yùn)行結(jié)果如何。這就是為什么
JUnit
使用組合模式的原因。
JUnit
為了采用組合模式將
TestCase
和
TestSuite
統(tǒng)一起來(lái),創(chuàng)建了一個(gè)
Test
接口來(lái)扮演抽象構(gòu)件角色,這樣原來(lái)的
TestCase
扮演組合模式中樹(shù)葉構(gòu)件角色,而
TestSuite
扮演組合模式中的樹(shù)枝構(gòu)件角色。下面將這三個(gè)類(lèi)的有關(guān)代碼分析如下:
?
//Test
接口
——
抽象構(gòu)件角色
public interface Test {
?????? /**
??????
?* Counts the number of test cases that will be run by this test.
??????
?*/
?????? public abstract int countTestCases();
?????? /**
??????
?* Runs a test and collects its result in a TestResult instance.
??????
?*/
?????? public abstract void run(TestResult result);
}
?
//TestSuite
類(lèi)的部分有關(guān)源碼
——Composite
角色,它實(shí)現(xiàn)了接口
Test
public class TestSuite implements Test {
//
用了較老的
Vector
來(lái)保存添加的
test
?????? private Vector fTests= new Vector(10);
?????? private String fName;
?????? ……?
/**
??????
?* Adds a test to the suite.
??????
?*/
?????? public void addTest(Test test) {??????????
//
注意這里的參數(shù)是
Test
類(lèi)型的。這就意味著
TestCase
和
TestSuite
以及以后實(shí)現(xiàn)
Test
接口的任何類(lèi)都可以被添加進(jìn)來(lái)
????????????? fTests.addElement(test);
?????? }
?????? ……
?????? /**
??????
?* Counts the number of test cases that will be run by this test.
??????
?*/
?????? public int countTestCases() {
????????????? int count= 0;
????????????? for (Enumeration e= tests(); e.hasMoreElements(); ) {
???????????????????? Test test= (Test)e.nextElement();
???????????????????? count= count + test.countTestCases();
????????????? }
????????????? return count;
?????? }
?????? /**
??????
?* Runs the tests and collects their result in a TestResult.
??????
?*/
?????? public void run(TestResult result) {
????????????? for (Enumeration e= tests(); e.hasMoreElements(); ) {
??????
?
?????????? if (result.shouldStop() )
??????
?
????????????????? break;
???????????????????? Test test= (Test)e.nextElement();
?????????????????????????? //關(guān)鍵在這個(gè)方法上面
???????????????????? runTest(test, result);
????????????? }
?????? }
????????????//這個(gè)方法里面就是遞歸的調(diào)用了,至于你的Test到底是什么類(lèi)型的只有在運(yùn)行的時(shí)候得知
????????????public void runTest(Test test, TestResult result) {
?????????????????? test.run(result);
????????????}
……
}
?
//TestCase
的部分有關(guān)源碼
——Leaf
角色,你編寫(xiě)的測(cè)試類(lèi)就是繼承自它
public abstract class TestCase extends Assert implements Test {
?????? ……
?????? /**
??????
?* Counts the number of test cases executed by run(TestResult result).
??????
?*/
?????? public int countTestCases() {
????????????? return 1;
?????? }
/**
??????
?* Runs the test case and collects the results in TestResult.
??????
?*/
?????? public void run(TestResult result) {
????????????? result.run(this);
?????? }
……
}
??????
可以看出這是一個(gè)偏重安全性的組合模式。因此在使用TestCase和TestSuite時(shí),不能使用Test來(lái)代替。
?
五、
優(yōu)缺點(diǎn)
從上面的舉例中可以看到,組合模式有以下優(yōu)點(diǎn):
1)????????
使客戶(hù)端調(diào)用簡(jiǎn)單,客戶(hù)端可以一致的使用組合結(jié)構(gòu)或其中單個(gè)對(duì)象,用戶(hù)就不必關(guān)心自己處理的是單個(gè)對(duì)象還是整個(gè)組合結(jié)構(gòu),這就簡(jiǎn)化了客戶(hù)端代碼。
2)??????
更容易在組合體內(nèi)加入對(duì)象部件
.
客戶(hù)端不必因?yàn)榧尤肓诵碌膶?duì)象部件而更改代碼。這一點(diǎn)符合開(kāi)閉原則的要求,對(duì)系統(tǒng)的二次開(kāi)發(fā)和功能擴(kuò)展很有利!
當(dāng)然組合模式也少不了缺點(diǎn):組合模式不容易限制組合中的構(gòu)件。
?
六、
總結(jié)
組合模式是一個(gè)應(yīng)用非常廣泛的設(shè)計(jì)模式,在前面已經(jīng)介紹過(guò)的解釋器模式、享元模式中都是用到了組合模式。它本身比較簡(jiǎn)單但是很有內(nèi)涵,掌握了它對(duì)你的開(kāi)發(fā)設(shè)計(jì)有很大的幫助。
這里寫(xiě)下了我學(xué)習(xí)組合模式的總結(jié),希望能給你帶來(lái)幫助,也希望您能給與指正。