一.什麼是組合模式?
組合模式提供了一種層級結構,並允許我們忽略物件與物件集合之間的差別
呼叫者並不知道手裡的東西是一個物件還是一組物件,不過沒關係,在組合模式中,呼叫者本來就不需要知道這些
二.舉個例子
假設我們要去描述檔案系統,檔案系統裡有檔案和資料夾,資料夾裡又有資料夾和檔案。。。
沒錯,這是一個層級結構,就像選單一樣,選單裡有選單項和子選單,子選單裡有選單項和子子選單。。層級結構也就是樹形結構,我們很容易想到定義一個 Node 類,包含一組指向孩子的指標,以此構造一顆完整的樹。那麼我們的類圖將是這樣的:

注意:File 僅支援類圖中列出的操作,Folder 類支援繼承來的所有操作
類的基本設計就是這樣,利用這樣的類結構就可以描述檔案系統了,下面來做程式碼實現:
定義 Directory 基類:
package CompositePattern;import java.util.ArrayList;
/**
-
定義目錄類
-
@author ayqy */ public abstract class Directory { String name; String description; ArrayList<Directory> files;
/**
- 新增指定檔案/資料夾到該目錄下
- @param dir 將要新增的檔案/資料夾
- @return 新增成功/失敗 */ public boolean add(Directory dir){ throw new UnsupportedOperationException();//預設擲回操作異常 }
/**
- 刪除該目錄下的指定檔案/資料夾
- @param dir 將要刪除的檔案/資料夾
- @return 刪除成功/失敗 */ public boolean remove(Directory dir){ throw new UnsupportedOperationException();//預設擲回操作異常 }
/**
- 清空該目錄下所有檔案和資料夾
- @return 清空成功/失敗 */ public boolean clear(){ throw new UnsupportedOperationException();//預設擲回操作異常 }
public ArrayList<Directory> getFiles() { throw new UnsupportedOperationException();//預設擲回操作異常 }
/**
- 列印輸出 */ public abstract void print();
public String getName() { return name; }
public String getDescription() { return description; }
public String toString(){ return name + description; } }
P.S.注意我們在基類中對 Folder 特有的方法的處理方式(擲回異常),當然也可以用更和諧的方式來做,各有各的好處與缺陷,在後文詳述採用擲回異常這樣粗暴的方式的原因
注意,我們在基類中定義了一個抽象的 print 方法,想用通過呼叫 print 方法來輸出整個檔案樹,組合模式允許我們以很輕鬆很優雅的方式實現這個麻煩的過程
下面來實現 File 類:
package CompositePattern;/**
-
實現檔案類
-
@author ayqy */ public class File extends Directory{
public File(String name, String desc) { this.name = name; this.description = desc; }
@Override public void print() { System.out.print(this.toString());//輸出檔案自身資訊 } }
File 類非常簡單,由於基類中對 File 不支援的操作都做了預設實現(擲回異常),所以 File 變得相當苗條
接下來是 Folder 類:
package CompositePattern;import java.util.ArrayList;
/**
-
實現資料夾類
-
@author ayqy */ public class Folder extends Directory{
public Folder(String name, String desc){ this.name = name; this.description = desc; this.files = new ArrayList<Directory>(); }
@Override public void print() { //列印該 Folder 自身資訊 System.out.print(this.toString() + "("); //列印該目錄下所有檔案及子檔案 for(Directory dir : files){ dir.print(); System.out.print(", "); } //列印資料夾遍歷結束標誌 System.out.print(")"); }
@Override public boolean add(Directory dir){ if(files.add(dir)) return true; else return false; }
@Override public boolean remove(Directory dir){ if(files.remove(dir)) return true; else return false; }
@Override public boolean clear(){ files.clear();
return true;}
@Override public ArrayList<Directory> getFiles() { return files; } }
Folder 類對所有支援的操作提供了自己的實現,並在 print 方法里做了點文章,用一個非常簡單的循環實現了對當前節點所有子孫節點的列印輸出(這容易讓人聯想到什麼?沒錯,是裝飾者模式),看起來似乎有些不可思議,不過這正是使用組合模式的好處之一(給遞迴提供了天然的土壤)
三.效果示例
上面實現了描述檔案系統所需的類,不妨測試一下,看看效果:
測試類程式碼如下:
package CompositePattern;/**
-
實現一個測試類
-
@author ayqy / public class Test { public static void main(String[] args){ /構造檔案樹/ / C a.txt b.txt system
sys.dat windows win32 settings log.txt win32.config */ Directory dir = new Folder("C", ""); dir.add(new File("a.txt", "")); dir.add(new File("b.txt", "")); Directory subDir = new Folder("system", ""); subDir.add(new File("sys.dat", "")); dir.add(subDir); Directory subDir2 = new Folder("windows", ""); Directory subDir3 = new Folder("win32", ""); subDir3.add(new Folder("settings", "")); subDir3.add(new File("log.txt", "")); subDir2.add(subDir3); subDir2.add(new File("win32.config", "")); dir.add(subDir2); dir.print();//列印輸出檔案樹} }
執行結果如下:
C(a.txt, b.txt, system(sys.dat, ), windows(win32(settings(), log.txt, ), win32.config, ), )
和我們預期的結果基本相同,但美中不足的是:存在多餘的逗號分隔符。要想消除多餘的逗號,我們就要顯示循環在走最後一趟時不輸出逗號,其餘時候都輸出一個逗號
很容易想到用一個顯式的迭代器來實現(hasNext 不正好用來判斷是不是最後一趟嗎?別忘了 ArrayList 是支援迭代器的),我們修改下 print 方法:
public void print() {
//列印該 Folder 自身資訊
System.out.print(this.toString() + "(");
//列印該目錄下所有檔案及子檔案
Iterator<Directory> iter = getFiles().iterator();
while(iter.hasNext()){
Directory dir = iter.next();
dir.print();
if(iter.hasNext()){
System.out.print(",");
}
}
//列印資料夾遍歷結束標誌
System.out.print(")");
}
成功消除了礙眼的多餘逗號
四.多一點改變
如何列印輸出所有關聯程式為 NotePad.exe 的檔案資訊
那麼現在先要給 File 新增一個新的屬性 linkedExe,表示與該檔案關聯的可執行程式,而資料夾則不支援這個屬性(在這裡我們規定資料夾不支援 linkedExe 屬性,不考慮與資料夾相關聯的程式是資源管理器還是別的什麼)
為了實現新的需求,我們不得不做一些改動了,為了獲得型別上的一致性,我們必須把 linkedExe 屬性新增到基類 Directory 中(這樣做或許會遭到詬病,但有些時候我們不得不犧牲一些好處來換取另一些好處。。)

矩形框中的內容是我們新增的新東西,這些東西都是 File 支援但 Folder 不支援的。做了這樣的變動之後,我們就可以列印輸出所有關聯程式為 NotePad.exe 的檔案資訊了。當然,還要修改 Folder 類的 print 方法:
public void print() {
//列印該目錄下所有關聯程式為 NotePad.exe 的檔案
for(Directory dir : files){
try{
if("NotePad.exe".equalsIgnoreCase(dir.getLinkedExe())){
dir.print();
}
}catch(UnsupportedOperationException e){
//吃掉異常,繼續遍歷(Folder 不支援 getLinkedExe 操作)
}
}
}
發現什麼了嗎?似乎組合模式的缺點越來越明顯了
組合模式要求忽略一個物件與一組物件之間的差異,一視同仁的對待它們
沒錯,照這樣做我們確實獲得了透明性(print 方法中我們並不知道當前正在處理的是一個 File 還是一個 Folder)。但我們甚至「濫用」異常處理機制來掩蓋物件集合與單一物件的差別,以追求「一視同仁」。而這樣做到底值不值得,需要視具體情況而定(我們總是在犧牲一些東西,以換取另一些更有用的東西,至於這種犧牲是否值得,當然需要權衡)
五.迭代器與組合模式
說好的迭代器呢?我怎麼沒有看到?它在哪裡?
迭代器就藏在組合模式中,我們的 print 方法內部不就一直在用迭代器嗎?(不是隱式迭代器就是顯示迭代器。。)
上面的例子中用的迭代器被稱為內部迭代器,也就是說,迭代器潛藏在組合模式的構成類中,所以不容易發現。當然,如果你喜歡的話也可以構造一個外部迭代器,就像這樣:

在 DirectoryIterator 中,我們需要手動維護一個棧結構來記錄當前的位置(內部迭代器是由系統棧提供的支援),以實現 hasNext 與 next 方法
其實還存在一個問題,File 類顯然不支援 iterator 方法,但它已經從父類繼承過來了,我們應該如何處理?
- 返回 null,那麼呼叫者必須使用 if 語句進行判斷
- 擲回異常,那麼呼叫者必須做異常處理
- (推薦做法)返回一個空迭代器(NullIterator),空迭代器如何實現?hasNext 直接返回 false 就好了。。這樣做對呼叫者沒有任何影響
六.總結
組合模式提供的樹形層次結構使得我們能夠一視同仁地對待單一物件與物件集合(獲得了操作上的方便),但這樣的好處是以犧牲類的單一責任原則換來的,而且組合模式是用繼承來實現的,缺少彈性。
所以在使用組合模式的時候應當慎重考慮,想想這樣的犧牲是否值得,如果不值得的話,考慮是不是可以用其它設計模式代替。。
七.一點題外話(關於是否擲回異常)
有些時候我們可以選擇返回 null,返回 false,返回錯誤碼等等而不是擲回異常,這些方式或許更和諧一些,但擲回異常在有些時候是對事實最貼切的表達
舉個例子,假設我們的 File 類有一個 hasLinkedExe 屬性,表示是否存在與之關聯的應用程式,而 Folder 不支援 hasLinkedExe 屬性,同時該屬性又是從父類繼承得到的,我們無法刪除它。
此時我們可以選擇返回 false 或者擲回異常:
- 返回 false:表示 Folder 沒有與之關聯的應用程式
- 擲回異常:表示 Folder 不支援該操作
顯然,擲回異常的含義才是我們真正想要表達的
說了這麼多,我們費了好大勁去用擲回異常的方式,好像只是為了表達的更貼切一些?不不不,絕對不要這樣想,這一點點意義上的差異可能會導致嚴重的問題,比如:
假設我們要輸出所有尚未關聯應用程式的檔案(即「未知檔案」)如果我們當初採用了返回 false 的方式來表示資料夾不支援此操作,那麼我們將會得到錯誤的結果(輸出了所有未知檔案和所有資料夾。。)
暫無評論,快來發表你的看法吧