JavaScript設計模式(八):組合模式

組合模式

組合模式:又叫 “部分整體” 模式,將對象組合成樹形結構,以表示 “部分-整體” 的層次結構。通過對象的多態性表現,使得用戶對單個對象和組合對象的使用具有一致性。

生活小栗子:文件目錄,DOM 文檔樹

模式特點

  1. 表示 “部分-整體” 的層次結構,生成 "樹葉型" 結構;
  2. 一致操作性,樹葉對象對外接口保存一致(操作與數據結構一致);
  3. 自上而下的的請求流向,從樹對象傳遞給葉對象;
  4. 調用頂層對象,會自行遍歷其下的葉對象執行。

樹葉型結構

代碼實現

樹對象和葉對象接口統一,樹對象增加一個緩存數組,存儲葉對象。執行樹對象方法時,將請求傳遞給其下葉對象執行。

// 樹對象 - 文件目錄
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
*/

CFolderCFile 接口保持一致。執行 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. 不只是簡單的子集遍歷

調用對象的接口方法時,如果該對象是樹對象,則會將請求傳遞給葉對象,由葉對象執行方法,以此類推。不同於迭代器模式,迭代器模式遍歷並不會做請求傳導。

應用場景

  1. 優化處理遞歸或分級數據結構(文件系統 - 目錄文件管理);
  2. 與其它設計模式聯用,如與命令模式聯用實現 “宏命令”。

優缺點

  • 優點:

    • 忽略組合對象和單個對象的差別,對外一致接口使用;
    • 解耦調用者與複雜元素之間的聯繫,處理方式變得簡單。
  • 缺點

    • 樹葉對象接口一致,無法區分,只有在運行時方可辨別;
    • 包裹對象創建太多,額外增加內存負擔。

參考文章

本文首發Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章