目錄
本文的結構如下:
- 引言
- 什么是組合模式
- 模式的結構
- 典型代碼
- 代碼示例
- 優點和缺點
- 適用環境
- 模式應用
一、引言
樹形結構是很常見的,比如目錄系統,隨便點開一個文件夾,文件夾下面可能有文件,也有子文件夾,子文件夾中還有子子文件夾和文件......
還有導航中的菜單。
還有公司的部門構造等,展開來看都是樹形的結構。
這些樹形結構在面向對象的世界中一般是用組合模式來處理的。
組合模式通過一種巧妙的設計方案,可以一致性地處理整個樹形結構或者樹形結構的一部分,也可以一致性地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點)。
二、什么是組合模式
對于樹形結構,當容器對象(如文件夾)的某一個方法被調用時,將遍歷整個樹形結構,尋找也包含這個方法的成員對象(可以是容器對象,也可以是葉子對象)并調用執行。這是靠遞歸調用的機制實現的。
由于容器對象和葉子對象在功能上的區別,在使用這些對象的代碼中必須有區別地對待容器對象和葉子對象,而實際上大多數情況下我們希望一致地處理它們,因為對于這些對象的區別對待將會使得程序非常復雜。組合模式為解決此類問題而誕生,它可以讓葉子對象和容器對象的使用具有一致性。
組合模式定義如下:
組合模式(Composite Pattern):組合多個對象形成樹形結構以表示具有“整體—部分”關系的層次結構。組合模式對單個對象(即葉子對象)和組合對象(即容器對象)的使用具有一致性,組合模式又可以稱為“整體—部分”(Part-Whole)模式,它是一種對象結構型模式。
三、模式的結構
組合模式的UML類圖如下:
在組合模式結構圖中包含如下幾個角色:
- Component(抽象構件):它可以是接口或抽象類,為葉子構件和容器構件對象聲明接口,在該角色中可以包含所有子類共有行為的聲明和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增加子構件、刪除子構件、獲取子構件等。
- Leaf(葉子構件):它在組合結構中表示葉子節點對象,葉子節點沒有子節點,它實現了在抽象構件中定義的行為。對于那些訪問及管理子構件的方法,可以通過異常等方式進行處理。
- Composite(容器構件):它在組合結構中表示容器節點對象,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用于存儲子節點,實現了在抽象構件中定義的行為,包括那些訪問及管理子構件的方法,在其業務方法中可以遞歸調用其子節點的業務方法。
組合模式的關鍵是定義了一個抽象構件類,它既可以代表葉子,又可以代表容器,而客戶端針對該抽象構件類進行編程,無須知道它到底表示的是葉子還是容器,可以對其進行統一處理。同時容器對象與抽象構件類之間還建立一個聚合關聯關系,在容器對象中既可以包含葉子,也可以包含容器,以此實現遞歸組合,形成一個樹形結構。
如果不使用組合模式,客戶端代碼將過多地依賴于容器對象復雜的內部實現結構,容器對象內部實現結構的變化將引起客戶代碼的頻繁變化,帶來了代碼維護復雜、可擴展性差等弊端。組合模式的引入將在一定程度上解決這些問題。
四、典型代碼
4.1、抽象構件角色
一般將抽象構件類設計為接口或抽象類,將所有子類共有方法的聲明和實現放在抽象構件類中。對于客戶端而言,將針對抽象構件編程,而無須關心其具體子類是容器構件還是葉子構件。
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();
}
/**
* 業務方法
*/
public void operation(){
throw new UnsupportedOperationException();
}
}
為什么所有方法都拋出UnsupportedOperationException?是因為有些方法只對容器構件有意義,而有些方法只對葉子構件有意義,這樣,如果某個子構件不支持某個操作,直接繼承默認方法就可以了。
4.2、葉子構件
葉子構件繼承自抽象構建抽象構建:
public class Leaf extends Component {
@Override
public void operation(){
System.out.println("子構件");
}
}
葉子構件不能再包含子構件,因此在葉子構件中只需事先業務方法,其他默認繼承,拋出為什么所有方法都拋出UnsupportedOperationException。
4.3、容器構件
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();
}
}
}
容器構件中實現了在抽象構件中聲明的所有方法,既包括業務方法,也包括用于訪問和管理成員子構件的方法。
需要注意的是在實現具體業務方法時,由于容器構件充當的是容器角色,包含成員構件,因此它將調用其成員構件的業務方法。在組合模式結構中,由于容器構件中仍然可以包含容器構件,因此在對容器構件進行處理時需要使用遞歸算法,即在容器構件的operation()方法中遞歸調用其成員構件的operation()方法。
五、代碼示例
假設這樣的場景:
在電腦E盤有個文件夾,該文件夾下面有很多文件,有視頻文件,有音頻文件,有圖像文件,還有包含視頻、音頻及圖像的文件夾,十分雜亂,現希望將這些雜亂的文件展示出來。
這里其實就是一個樹形結構,根據一定的規則分類后,大致是這樣的:
5.1、不使用組合模式
注:當然可以一個循環遍歷就搞定了,因為這里用的是文件的形式,如果是別的形式呢?所以不要太較真了,只是舉例。
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;
//音樂
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();
}
}
}
客戶端測試:
public class Client {
public static void main(String[] args) {
MusicFile m1 = new MusicFile("盡頭.mp3");
MusicFile m2 = new MusicFile("飄洋過海來看你.mp3");
MusicFile m3 = new MusicFile("曾經的你.mp3");
MusicFile m4 = new MusicFile("take me to your heart.mp3");
VideoFile v1 = new VideoFile("戰狼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();
}
}
如果采用上述的形式,有幾個缺點:
- 文件夾類Folder的設計和實現都非常復雜,需要定義多個集合存儲不同類型的成員,而且需要針對不同的成員提供增加、刪除和獲取等管理和訪問成員的方法,存在大量的冗余代碼,系統維護較為困難;
- 由于系統沒有提供抽象層,客戶端代碼必須有區別地對待充當容器的文件夾Folder和充當葉子的MusicFile、ImageFile和VideoFile,無法統一對它們進行處理;
- 系統的靈活性和可擴展性差,如果增加了新的類型的葉子和容器都需要對原有代碼進行修改,例如如果需要在系統中增加一種新類型的文本文件TextFile,則必須修改Folder類的源代碼,否則無法在文件夾中添加文本文件。
5.2、使用組合模式改進
為了讓系統具有更好的靈活性和可擴展性,客戶端可以一致地對待文件和文件夾,定義一個抽象構件AbstractFile,Folder充當容器構件,MusicFile、VideoFile和ImageFile充當葉子構件。
抽象構件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();
}
}
葉子構件:
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);
}
}
容器構件:
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();
}
}
}
客戶端測試:
public class Client {
public static void main(String[] args) {
AbstractFile m1 = new MusicFile("盡頭.mp3");
AbstractFile m2 = new MusicFile("飄洋過海來看你.mp3");
AbstractFile m3 = new MusicFile("曾經的你.mp3");
AbstractFile m4 = new MusicFile("take me to your heart.mp3");
AbstractFile v1 = new VideoFile("戰狼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();
}
}
用組合模式提供一個抽象構件后,客戶端可以一致對待容器構件和葉子構件,進行統一處理,并且大量減少了冗余,擴展性也很好,新增TextFile無需修改Folder源碼,只需修改客戶端即可。
當然,這里似乎有點違法“迭代器模式”中講的“單一職責原則”,的確是,抽象構件不但要管理層次結構,還要執行一些業務操作。
我覺得應該這樣理解問題,設計模式并不應該是生套似的,各種設計原則也并不是說一定不能破壞的,所有的種種都是為了更好的解決問題,更好的進行擴展維護,當適度破壞既定的原則,卻可以更好的解決問題時,顯然這里以單一設計原則換取了透明性,這種折中方案是可取的。
六、優點和缺點
6.1、優點
組合模式的主要優點如下:
- 組合模式可以清楚地定義分層次的復雜對象,表示對象的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
- 客戶端可以一致地使用一個組合結構或其中單個對象,不必關心處理的是單個對象還是整個組合結構,簡化了客戶端代碼。
- 在組合模式中增加新的容器構件和葉子構件都很方便,無須對現有類庫進行任何修改,符合“開閉原則”。
- 組合模式為樹形結構的面向對象實現提供了一種靈活的解決方案,通過葉子對象和容器對象的遞歸組合,可以形成復雜的樹形結構,但對樹形結構的控制卻非常簡單。
6.2、缺點
組合模式的主要缺點如下:
- 破壞了“單一職責原則”。
- 在增加新構件時很難對容器中的構件類型進行限制。有時候我們希望一個容器中只能有某些特定類型的對象,例如在某個文件夾中只能包含文本文件,使用組合模式時,不能依賴類型系統來施加這些約束,因為它們都來自于相同的抽象層,在這種情況下,必須通過在運行時進行類型檢查來實現,這個實現過程較為復雜。
七、適用環境
在以下情況下可以考慮使用組合模式:
- 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待它們。
- 在一個使用面向對象語言開發的系統中需要處理一個樹形結構。
- 在一個系統中能夠分離出葉子對象和容器對象,而且它們的類型不固定,需要增加一些新的類型。
八、模式應用
組合模式使用面向對象的思想來實現樹形結構的構建與處理,描述了如何將容器對象和葉子對象進行遞歸組合,實現簡單,靈活性好。由于在軟件開發中存在大量的樹形結構,因此組合模式是一種使用頻率較高的結構型設計模式,Java SE中的AWT和Swing包的設計就基于組合模式,在這些界面包中為用戶提供了大量的容器構件(如Container)和成員構件(如Checkbox、Button和TextArea等)。