組合模式:又叫 “部分整體” 模式,將對象組合成樹形結構,以表示 “部分-整體” 的層次結構。通過對象的多態性表現,使得用戶對單個對象和組合對象的使用具有一致性。
生活小栗子:文件目錄,DOM 文檔樹
模式特點
- 表示 “部分-整體” 的層次結構,生成 "樹葉型" 結構;
- 一致操作性,樹葉對象對外接口保存一致(操作與數據結構一致);
- 自上而下的的請求流向,從樹對象傳遞給葉對象;
- 調用頂層對象,會自行遍歷其下的葉對象執行。
代碼實現
樹對象和葉對象接口統一,樹對象增加一個緩存數組,存儲葉對象。執行樹對象方法時,將請求傳遞給其下葉對象執行。
// 樹對象 - 文件目錄
class CFolder {
constructor(name) {
this.name = name;
this.files = [];
}
add(file) {
this.files.push(file);
}
scan() {
for (let file of this.files) {
file.scan();
}
}
}
// 葉對象 - 文件
class CFile {
constructor(name) {
this.name = name;
}
add(file) {
throw new Error('文件下面不能再添加文件');
}
scan() {
console.log(`開始掃描文件:${this.name}`);
}
}
let mediaFolder = new CFolder('娛樂');
let movieFolder = new CFolder('電影');
let musicFolder = new CFolder('音樂');
let file1 = new CFile('鋼鐵俠.mp4');
let file2 = new CFile('再談記憶.mp3');
movieFolder.add(file1);
musicFolder.add(file2);
mediaFolder.add(movieFolder);
mediaFolder.add(musicFolder);
mediaFolder.scan();
/* 輸出:
開始掃描文件:鋼鐵俠.mp4
開始掃描文件:再談記憶.mp3
*/
CFolder
與 CFile
接口保持一致。執行 scan()
時,若發現是樹對象,則繼續遍歷其下的葉對象,執行 scan()
。
JavaScript 不同於其它靜態編程語言,實現組合模式的難點是保持樹對象與葉對象之間接口保持統一,可藉助 TypeScript 定製接口規範,實現類型約束。
// 定義接口規範
interface Compose {
name: string,
add(file: CFile): void,
scan(): void
}
// 樹對象 - 文件目錄
class CFolder implements Compose {
fileList = [];
name: string;
constructor(name: string) {
this.name = name;
}
add(file: CFile) {
this.fileList.push(file);
}
scan() {
for (let file of this.fileList) {
file.scan();
}
}
}
// 葉對象 - 文件
class CFile implements Compose {
name: string;
constructor(name: string) {
this.name = name;
}
add(file: CFile) {
throw new Error('文件下面不能再添加文件');
}
scan() {
console.log(`開始掃描:${this.name}`)
}
}
let mediaFolder = new CFolder('娛樂');
let movieFolder = new CFolder('電影');
let musicFolder = new CFolder('音樂');
let file1 = new CFile('鋼鐵俠.mp4');
let file2 = new CFile('再談記憶.mp3');
movieFolder.add(file1);
musicFolder.add(file2);
mediaFolder.add(movieFolder);
mediaFolder.add(musicFolder);
mediaFolder.scan();
/* 輸出:
開始掃描文件:鋼鐵俠.mp4
開始掃描文件:再談記憶.mp3
*/
透明性的安全問題
組合模式的透明性,指的是樹葉對象接口保持統一,外部調用時無需區分。但是這會帶來一些問題,如上述文件目錄的例子,文件(葉對象)下不可再添加文件,因此需在文件類的 add()
方法中拋出異常,以作提醒。
誤區規避
1. 組合不是繼承,樹葉對象並不是父子對象
組合模式的樹型結構是一種 HAS-A(聚合)的關係,而不是 IS-A 。樹葉對象能夠合作的關鍵,是它們對外保持統一接口,而不是葉對象繼承樹對象的屬性方法,兩者之間不是父子關係。
2. 葉對象操作保持一致性
葉對象除了與樹對象接口一致外,操作也必須保持一致性。一片葉子只能生在一顆樹上。調用頂層對象時,每個葉對象只能接收一次請求,一個葉對象不能從屬多個樹對象。
3. 葉對象實現冒泡傳遞
請求傳遞由樹向葉傳遞,如果想逆轉傳遞過程,需在葉對象中保留對樹對象的引用,冒泡傳遞給樹對象處理。
4. 不只是簡單的子集遍歷
調用對象的接口方法時,如果該對象是樹對象,則會將請求傳遞給葉對象,由葉對象執行方法,以此類推。不同於迭代器模式,迭代器模式遍歷並不會做請求傳導。
應用場景
- 優化處理遞歸或分級數據結構(文件系統 - 目錄文件管理);
- 與其它設計模式聯用,如與命令模式聯用實現 “宏命令”。
優缺點
-
優點:
- 忽略組合對象和單個對象的差別,對外一致接口使用;
- 解耦調用者與複雜元素之間的聯繫,處理方式變得簡單。
-
缺點
- 樹葉對象接口一致,無法區分,只有在運行時方可辨別;
- 包裹對象創建太多,額外增加內存負擔。
參考文章
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。