目錄
本文的結(jié)構(gòu)如下:
- 引言
- 什么是組合模式
- 模式的結(jié)構(gòu)
- 典型代碼
- 代碼示例
- 優(yōu)點(diǎn)和缺點(diǎn)
- 適用環(huán)境
- 模式應(yīng)用
一、引言
樹(shù)形結(jié)構(gòu)是很常見(jiàn)的,比如目錄系統(tǒng),隨便點(diǎn)開(kāi)一個(gè)文件夾,文件夾下面可能有文件,也有子文件夾,子文件夾中還有子子文件夾和文件......
還有導(dǎo)航中的菜單。
還有公司的部門構(gòu)造等,展開(kāi)來(lái)看都是樹(shù)形的結(jié)構(gòu)。
這些樹(shù)形結(jié)構(gòu)在面向?qū)ο蟮氖澜缰幸话闶怯?strong>組合模式來(lái)處理的。
組合模式通過(guò)一種巧妙的設(shè)計(jì)方案,可以一致性地處理整個(gè)樹(shù)形結(jié)構(gòu)或者樹(shù)形結(jié)構(gòu)的一部分,也可以一致性地處理樹(shù)形結(jié)構(gòu)中的葉子節(jié)點(diǎn)(不包含子節(jié)點(diǎn)的節(jié)點(diǎn))和容器節(jié)點(diǎn)(包含子節(jié)點(diǎn)的節(jié)點(diǎn))。
二、什么是組合模式
對(duì)于樹(shù)形結(jié)構(gòu),當(dāng)容器對(duì)象(如文件夾)的某一個(gè)方法被調(diào)用時(shí),將遍歷整個(gè)樹(shù)形結(jié)構(gòu),尋找也包含這個(gè)方法的成員對(duì)象(可以是容器對(duì)象,也可以是葉子對(duì)象)并調(diào)用執(zhí)行。這是靠遞歸調(diào)用的機(jī)制實(shí)現(xiàn)的。
由于容器對(duì)象和葉子對(duì)象在功能上的區(qū)別,在使用這些對(duì)象的代碼中必須有區(qū)別地對(duì)待容器對(duì)象和葉子對(duì)象,而實(shí)際上大多數(shù)情況下我們希望一致地處理它們,因?yàn)閷?duì)于這些對(duì)象的區(qū)別對(duì)待將會(huì)使得程序非常復(fù)雜。組合模式為解決此類問(wèn)題而誕生,它可以讓葉子對(duì)象和容器對(duì)象的使用具有一致性。
組合模式定義如下:
組合模式(Composite Pattern):組合多個(gè)對(duì)象形成樹(shù)形結(jié)構(gòu)以表示具有“整體—部分”關(guān)系的層次結(jié)構(gòu)。組合模式對(duì)單個(gè)對(duì)象(即葉子對(duì)象)和組合對(duì)象(即容器對(duì)象)的使用具有一致性,組合模式又可以稱為“整體—部分”(Part-Whole)模式,它是一種對(duì)象結(jié)構(gòu)型模式。
三、模式的結(jié)構(gòu)
組合模式的UML類圖如下:
在組合模式結(jié)構(gòu)圖中包含如下幾個(gè)角色:
- Component(抽象構(gòu)件):它可以是接口或抽象類,為葉子構(gòu)件和容器構(gòu)件對(duì)象聲明接口,在該角色中可以包含所有子類共有行為的聲明和實(shí)現(xiàn)。在抽象構(gòu)件中定義了訪問(wèn)及管理它的子構(gòu)件的方法,如增加子構(gòu)件、刪除子構(gòu)件、獲取子構(gòu)件等。
- Leaf(葉子構(gòu)件):它在組合結(jié)構(gòu)中表示葉子節(jié)點(diǎn)對(duì)象,葉子節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn),它實(shí)現(xiàn)了在抽象構(gòu)件中定義的行為。對(duì)于那些訪問(wèn)及管理子構(gòu)件的方法,可以通過(guò)異常等方式進(jìn)行處理。
- Composite(容器構(gòu)件):它在組合結(jié)構(gòu)中表示容器節(jié)點(diǎn)對(duì)象,容器節(jié)點(diǎn)包含子節(jié)點(diǎn),其子節(jié)點(diǎn)可以是葉子節(jié)點(diǎn),也可以是容器節(jié)點(diǎn),它提供一個(gè)集合用于存儲(chǔ)子節(jié)點(diǎn),實(shí)現(xiàn)了在抽象構(gòu)件中定義的行為,包括那些訪問(wèn)及管理子構(gòu)件的方法,在其業(yè)務(wù)方法中可以遞歸調(diào)用其子節(jié)點(diǎn)的業(yè)務(wù)方法。
組合模式的關(guān)鍵是定義了一個(gè)抽象構(gòu)件類,它既可以代表葉子,又可以代表容器,而客戶端針對(duì)該抽象構(gòu)件類進(jìn)行編程,無(wú)須知道它到底表示的是葉子還是容器,可以對(duì)其進(jìn)行統(tǒng)一處理。同時(shí)容器對(duì)象與抽象構(gòu)件類之間還建立一個(gè)聚合關(guān)聯(lián)關(guān)系,在容器對(duì)象中既可以包含葉子,也可以包含容器,以此實(shí)現(xiàn)遞歸組合,形成一個(gè)樹(shù)形結(jié)構(gòu)。
如果不使用組合模式,客戶端代碼將過(guò)多地依賴于容器對(duì)象復(fù)雜的內(nèi)部實(shí)現(xiàn)結(jié)構(gòu),容器對(duì)象內(nèi)部實(shí)現(xiàn)結(jié)構(gòu)的變化將引起客戶代碼的頻繁變化,帶來(lái)了代碼維護(hù)復(fù)雜、可擴(kuò)展性差等弊端。組合模式的引入將在一定程度上解決這些問(wèn)題。
四、典型代碼
4.1、抽象構(gòu)件角色
一般將抽象構(gòu)件類設(shè)計(jì)為接口或抽象類,將所有子類共有方法的聲明和實(shí)現(xiàn)放在抽象構(gòu)件類中。對(duì)于客戶端而言,將針對(duì)抽象構(gòu)件編程,而無(wú)須關(guān)心其具體子類是容器構(gòu)件還是葉子構(gòu)件。
public abstract class Component {
/**
* 增加成員
* @param c
*/
public void add(Component c){
throw new UnsupportedOperationException();
}
/**
* 刪除成員
* @param c
*/
public void remove(Component c){
throw new UnsupportedOperationException();
}
/**
* 獲取成員
* @param i
* @return
*/
public Component getChild(int i){
throw new UnsupportedOperationException();
}
/**
* 業(yè)務(wù)方法
*/
public void operation(){
throw new UnsupportedOperationException();
}
}
為什么所有方法都拋出UnsupportedOperationException?是因?yàn)橛行┓椒ㄖ粚?duì)容器構(gòu)件有意義,而有些方法只對(duì)葉子構(gòu)件有意義,這樣,如果某個(gè)子構(gòu)件不支持某個(gè)操作,直接繼承默認(rèn)方法就可以了。
4.2、葉子構(gòu)件
葉子構(gòu)件繼承自抽象構(gòu)建抽象構(gòu)建:
public class Leaf extends Component {
@Override
public void operation(){
System.out.println("子構(gòu)件");
}
}
葉子構(gòu)件不能再包含子構(gòu)件,因此在葉子構(gòu)件中只需事先業(yè)務(wù)方法,其他默認(rèn)繼承,拋出為什么所有方法都拋出UnsupportedOperationException。
4.3、容器構(gòu)件
public class Composite extends Component {
private List<Component> list = new ArrayList<Component>();
@Override
public void add(Component c){
list.add(c);
}
@Override
public void remove(Component c) {
list.remove(c);
}
@Override
public Component getChild(int i) {
return list.get(i);
}
@Override
public void operation(){
for (Component child: list){
child.operation();
}
}
}
容器構(gòu)件中實(shí)現(xiàn)了在抽象構(gòu)件中聲明的所有方法,既包括業(yè)務(wù)方法,也包括用于訪問(wèn)和管理成員子構(gòu)件的方法。
需要注意的是在實(shí)現(xiàn)具體業(yè)務(wù)方法時(shí),由于容器構(gòu)件充當(dāng)?shù)氖侨萜鹘巧蓡T構(gòu)件,因此它將調(diào)用其成員構(gòu)件的業(yè)務(wù)方法。在組合模式結(jié)構(gòu)中,由于容器構(gòu)件中仍然可以包含容器構(gòu)件,因此在對(duì)容器構(gòu)件進(jìn)行處理時(shí)需要使用遞歸算法,即在容器構(gòu)件的operation()方法中遞歸調(diào)用其成員構(gòu)件的operation()方法。
五、代碼示例
假設(shè)這樣的場(chǎng)景:
在電腦E盤有個(gè)文件夾,該文件夾下面有很多文件,有視頻文件,有音頻文件,有圖像文件,還有包含視頻、音頻及圖像的文件夾,十分雜亂,現(xiàn)希望將這些雜亂的文件展示出來(lái)。
這里其實(shí)就是一個(gè)樹(shù)形結(jié)構(gòu),根據(jù)一定的規(guī)則分類后,大致是這樣的:
5.1、不使用組合模式
注:當(dāng)然可以一個(gè)循環(huán)遍歷就搞定了,因?yàn)檫@里用的是文件的形式,如果是別的形式呢?所以不要太較真了,只是舉例。
public class MusicFile {
private String name;
public MusicFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class VideoFile {
private String name;
public VideoFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class ImageFile {
private String name;
public ImageFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class Folder {
private String name;
//音樂(lè)
private List<MusicFile> musicList = new ArrayList<MusicFile>();
//視頻
private List<VideoFile> videoList = new ArrayList<VideoFile>();
//圖片
private List<ImageFile> imageList = new ArrayList<ImageFile>();
//文件夾
private List<Folder> folderList = new ArrayList<Folder>();
public Folder(String name){
this.name = name;
}
public void addFolder(Folder folder){
folderList.add(folder);
}
public void addImage(ImageFile image){
imageList.add(image);
}
public void addVideo(VideoFile video){
videoList.add(video);
}
public void addMusic(MusicFile music){
musicList.add(music);
}
public void print(){
for (MusicFile music : musicList){
music.print();
}
for (VideoFile video : videoList){
video.print();
}
for(ImageFile image : imageList){
image.print();
}
for (Folder folder : folderList){
folder.print();
}
}
}
客戶端測(cè)試:
public class Client {
public static void main(String[] args) {
MusicFile m1 = new MusicFile("盡頭.mp3");
MusicFile m2 = new MusicFile("飄洋過(guò)海來(lái)看你.mp3");
MusicFile m3 = new MusicFile("曾經(jīng)的你.mp3");
MusicFile m4 = new MusicFile("take me to your heart.mp3");
VideoFile v1 = new VideoFile("戰(zhàn)狼2.mp4");
VideoFile v2 = new VideoFile("理想.avi");
VideoFile v3 = new VideoFile("瑯琊榜.avi");
ImageFile i1 = new ImageFile("敦煌.png");
ImageFile i2 = new ImageFile("baby.jpg");
ImageFile i3 = new ImageFile("girl.jpg");
Folder aa = new Folder("aa");
aa.addImage(i3);
Folder bb = new Folder("bb");
bb.addMusic(m4);
bb.addVideo(v3);
Folder top = new Folder("top");
top.addFolder(aa);
top.addFolder(bb);
top.addMusic(m1);
top.addMusic(m2);
top.addMusic(m3);
top.addVideo(v1);
top.addVideo(v2);
top.addImage(i1);
top.addImage(i2);
top.print();
}
}
如果采用上述的形式,有幾個(gè)缺點(diǎn):
- 文件夾類Folder的設(shè)計(jì)和實(shí)現(xiàn)都非常復(fù)雜,需要定義多個(gè)集合存儲(chǔ)不同類型的成員,而且需要針對(duì)不同的成員提供增加、刪除和獲取等管理和訪問(wèn)成員的方法,存在大量的冗余代碼,系統(tǒng)維護(hù)較為困難;
- 由于系統(tǒng)沒(méi)有提供抽象層,客戶端代碼必須有區(qū)別地對(duì)待充當(dāng)容器的文件夾Folder和充當(dāng)葉子的MusicFile、ImageFile和VideoFile,無(wú)法統(tǒng)一對(duì)它們進(jìn)行處理;
- 系統(tǒng)的靈活性和可擴(kuò)展性差,如果增加了新的類型的葉子和容器都需要對(duì)原有代碼進(jìn)行修改,例如如果需要在系統(tǒng)中增加一種新類型的文本文件TextFile,則必須修改Folder類的源代碼,否則無(wú)法在文件夾中添加文本文件。
5.2、使用組合模式改進(jìn)
為了讓系統(tǒng)具有更好的靈活性和可擴(kuò)展性,客戶端可以一致地對(duì)待文件和文件夾,定義一個(gè)抽象構(gòu)件AbstractFile,F(xiàn)older充當(dāng)容器構(gòu)件,MusicFile、VideoFile和ImageFile充當(dāng)葉子構(gòu)件。
抽象構(gòu)件AbstractFile:
public abstract class AbstractFile {
public void add(AbstractFile file){
throw new UnsupportedOperationException();
}
public void remove(AbstractFile file){
throw new UnsupportedOperationException();
}
public AbstractFile getChild(int i){
throw new UnsupportedOperationException();
}
public void print(){
throw new UnsupportedOperationException();
}
}
葉子構(gòu)件:
public class MusicFile extends AbstractFile{
private String name;
public MusicFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class VideoFile extends AbstractFile{
private String name;
public VideoFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class ImageFile extends AbstractFile{
private String name;
public ImageFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
容器構(gòu)件:
public class Folder extends AbstractFile{
private String name;
private List<AbstractFile> files = new ArrayList<AbstractFile>();
public Folder(String name){
this.name = name;
}
@Override
public void add(AbstractFile file){
files.add(file);
}
@Override
public void remove(AbstractFile file){
files.remove(file);
}
@Override
public AbstractFile getChild(int i){
return files.get(i);
}
@Override
public void print(){
for (AbstractFile file : files){
file.print();
}
}
}
客戶端測(cè)試:
public class Client {
public static void main(String[] args) {
AbstractFile m1 = new MusicFile("盡頭.mp3");
AbstractFile m2 = new MusicFile("飄洋過(guò)海來(lái)看你.mp3");
AbstractFile m3 = new MusicFile("曾經(jīng)的你.mp3");
AbstractFile m4 = new MusicFile("take me to your heart.mp3");
AbstractFile v1 = new VideoFile("戰(zhàn)狼2.mp4");
AbstractFile v2 = new VideoFile("理想.avi");
AbstractFile v3 = new VideoFile("瑯琊榜.avi");
AbstractFile i1 = new ImageFile("敦煌.png");
AbstractFile i2 = new ImageFile("baby.jpg");
AbstractFile i3 = new ImageFile("girl.jpg");
AbstractFile aa = new Folder("aa");
aa.add(i3);
AbstractFile bb = new Folder("bb");
bb.add(m4);
bb.add(v3);
AbstractFile top = new Folder("top");
top.add(aa);
top.add(bb);
top.add(m1);
top.add(m2);
top.add(m3);
top.add(v1);
top.add(v2);
top.add(i1);
top.add(i2);
top.print();
}
}
用組合模式提供一個(gè)抽象構(gòu)件后,客戶端可以一致對(duì)待容器構(gòu)件和葉子構(gòu)件,進(jìn)行統(tǒng)一處理,并且大量減少了冗余,擴(kuò)展性也很好,新增TextFile無(wú)需修改Folder源碼,只需修改客戶端即可。
當(dāng)然,這里似乎有點(diǎn)違法“迭代器模式”中講的“單一職責(zé)原則”,的確是,抽象構(gòu)件不但要管理層次結(jié)構(gòu),還要執(zhí)行一些業(yè)務(wù)操作。
我覺(jué)得應(yīng)該這樣理解問(wèn)題,設(shè)計(jì)模式并不應(yīng)該是生套似的,各種設(shè)計(jì)原則也并不是說(shuō)一定不能破壞的,所有的種種都是為了更好的解決問(wèn)題,更好的進(jìn)行擴(kuò)展維護(hù),當(dāng)適度破壞既定的原則,卻可以更好的解決問(wèn)題時(shí),顯然這里以單一設(shè)計(jì)原則換取了透明性,這種折中方案是可取的。
六、優(yōu)點(diǎn)和缺點(diǎn)
6.1、優(yōu)點(diǎn)
組合模式的主要優(yōu)點(diǎn)如下:
- 組合模式可以清楚地定義分層次的復(fù)雜對(duì)象,表示對(duì)象的全部或部分層次,它讓客戶端忽略了層次的差異,方便對(duì)整個(gè)層次結(jié)構(gòu)進(jìn)行控制。
- 客戶端可以一致地使用一個(gè)組合結(jié)構(gòu)或其中單個(gè)對(duì)象,不必關(guān)心處理的是單個(gè)對(duì)象還是整個(gè)組合結(jié)構(gòu),簡(jiǎn)化了客戶端代碼。
- 在組合模式中增加新的容器構(gòu)件和葉子構(gòu)件都很方便,無(wú)須對(duì)現(xiàn)有類庫(kù)進(jìn)行任何修改,符合“開(kāi)閉原則”。
- 組合模式為樹(shù)形結(jié)構(gòu)的面向?qū)ο髮?shí)現(xiàn)提供了一種靈活的解決方案,通過(guò)葉子對(duì)象和容器對(duì)象的遞歸組合,可以形成復(fù)雜的樹(shù)形結(jié)構(gòu),但對(duì)樹(shù)形結(jié)構(gòu)的控制卻非常簡(jiǎn)單。
6.2、缺點(diǎn)
組合模式的主要缺點(diǎn)如下:
- 破壞了“單一職責(zé)原則”。
- 在增加新構(gòu)件時(shí)很難對(duì)容器中的構(gòu)件類型進(jìn)行限制。有時(shí)候我們希望一個(gè)容器中只能有某些特定類型的對(duì)象,例如在某個(gè)文件夾中只能包含文本文件,使用組合模式時(shí),不能依賴類型系統(tǒng)來(lái)施加這些約束,因?yàn)樗鼈兌紒?lái)自于相同的抽象層,在這種情況下,必須通過(guò)在運(yùn)行時(shí)進(jìn)行類型檢查來(lái)實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)過(guò)程較為復(fù)雜。
七、適用環(huán)境
在以下情況下可以考慮使用組合模式:
- 在具有整體和部分的層次結(jié)構(gòu)中,希望通過(guò)一種方式忽略整體與部分的差異,客戶端可以一致地對(duì)待它們。
- 在一個(gè)使用面向?qū)ο笳Z(yǔ)言開(kāi)發(fā)的系統(tǒng)中需要處理一個(gè)樹(shù)形結(jié)構(gòu)。
- 在一個(gè)系統(tǒng)中能夠分離出葉子對(duì)象和容器對(duì)象,而且它們的類型不固定,需要增加一些新的類型。
八、模式應(yīng)用
組合模式使用面向?qū)ο蟮乃枷雭?lái)實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)的構(gòu)建與處理,描述了如何將容器對(duì)象和葉子對(duì)象進(jìn)行遞歸組合,實(shí)現(xiàn)簡(jiǎn)單,靈活性好。由于在軟件開(kāi)發(fā)中存在大量的樹(shù)形結(jié)構(gòu),因此組合模式是一種使用頻率較高的結(jié)構(gòu)型設(shè)計(jì)模式,Java SE中的AWT和Swing包的設(shè)計(jì)就基于組合模式,在這些界面包中為用戶提供了大量的容器構(gòu)件(如Container)和成員構(gòu)件(如Checkbox、Button和TextArea等)。