組合模式

1. 回顧宏命令

咱們在命令模式中講解過宏命令的結構和做用。宏命令對象包含了一組具體的子命令對象,無論是宏命令對象,仍是子命令對象,都有一個execute方法負責執行命令。如今回顧一下這段安裝在萬能遙控器上的宏命令代碼。html

var closeDoorCommand = {
    execute: function () {
    console.log('關門');
    }
};
var openPcCommand = {
    execute: function () {
        console.log('開電腦');
    }
};
var openQQCommand = {
    execute: function () {
        console.log('上QQ');
    }
};

var MacroCommand = function () {
    return {
        commandList: [],
        add: function (command) {
            this.commandList.push(command);
        },
        execute: function () {
            for (var i = 0, command; command = this.commandList[i++];) {
                command.execute();
            }
        }
    };
};

var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);

macroCommand.execute();

經過觀察這段代碼,咱們很容易發現,宏命令中包含了一組子命令,它們組成了一個樹形結構,這裏是一顆結構很是簡單的樹,以下圖所示(不堪入目的PS):程序員

其中,macroCommand被稱爲組合對象,closeDoorCommand,openPCCommand,openQQCommand都是葉對象。在macroCommand的execute方法裏,並不執行真正的操做,而是遍歷它所包含的葉對象,把真正的execute請求委託給這些葉對象。
macroCommand表現的像一個命令,但它實際上只是一組真正命令的「代理」。並不是真正的代理,雖然結構上類似,但macroCommand只負責傳遞請求給葉對象,它的目的不在於控制對葉對象的訪問。編程

2. 組合模式的用途

組合模式將對象組合成樹形結構,以表示「部分——總體」的層次結構。除了用來表示樹形結構以外,組合模式的另外一個好處是經過對象的多態性表現,使得用戶對單個對象和組合對象的使用具備一致性,下面分別說明。設計模式

  • 表示樹形結構。經過回顧上面的例子,咱們很容易找到組合模式的一個優勢:提供了一種遍歷樹形結構的方案,經過調用組合對象的execute方法,程序會遞歸調用組合對象下面的葉對象的execute方法,因此咱們的萬能遙控器只須要一次操做,便能依次完成關門,打開電腦,上QQ這幾件事情。組合模式能夠很是方便地描述對象部分——總體層次結構。
  • 利用對象多態性統一對待組合對象和單個對象。利用對象的多態性表現,可使客戶端忽略組合對象和單個對象的不一樣。在組合模式中,客戶將統一地使用組合結構中的全部對象,而不須要關心它到底是組合對象仍是單個對象。

這在實際開發中會給客戶帶來至關大的便利性,當咱們往萬能遙控器裏面添加一個命令的時候,並不關心這個命令是宏命令仍是普通子命令。這點對於咱們不重要,咱們只須要肯定它是一個命令,而且這個命令擁有可執行的execute方法,那麼這個命令就能夠被添加進萬能遙控器。
當宏命令和普通子命令接收到執行execute方法的請求時,宏命令和普通子命令都會作它們各自認爲正確的事情。這些差別是隱藏在客戶背後的,在客戶看來,這種透明性可讓咱們很是自由地擴展這個萬能遙控器。安全

3. 請求在樹中傳遞的過程

在組合模式中,請求在樹中傳遞的過程老是遵循一種邏輯。
以宏命令爲例,請求從樹中最頂端的對象往下傳遞,若是當前處理請求的對象是葉對象(普通子命令),葉對象自身會對請求做出相應的處理:若是當前處理請求的對象是組合對象(宏命令),組合對象則會遍歷它屬下的子節點,將請求繼續傳遞給這些子節點。
總而言之,若是子節點是葉對象,葉對象自身會處理這個請求,而若是子節點仍是組合對象,請求會繼續往下傳遞。葉對象下面不會再有其它子節點,一個葉對象就是樹的這條枝葉的盡頭,組合對象下面可能還會有子節點,如圖所示:架構

請求從上到下沿着樹進行傳遞,直到樹的盡頭。做爲客戶,只須要關心樹最頂層的組合對象,客戶只須要請求這個組合對象,請求便會沿着樹往下傳遞,依次到達全部的葉對象。
在剛剛的例子中,因爲宏命令和子命令組成的樹太過簡單,咱們還不能清楚的看到組合模式帶來的好處,若是隻是簡單的遍歷一組子節點,迭代器便能解決全部的問題。接下來咱們將創造一個更強大的宏命令,這個宏命令中又包含了另一些宏命令和普通子命令,看起來是一顆相對較複雜的樹。函數

4. 更強大的宏命令

目前的萬能遙控器,包含了關門,開電腦,上QQ這3個命令。如今咱們須要一個「超級萬能遙控器」,能夠控制家裏全部的電器,這個遙控器擁有打開空調,打開電視和音響,關門,開電腦,登錄QQ這些功能。
首先在節點中放置一個按鈕button來表示這個超級萬能遙控器,超級萬能遙控器上安裝了一個宏命令,當執行這個宏命令時,會依次遍歷執行它所包含的子命令,代碼以下:性能

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>
    <button id="button">dian我</button>
<script>
    var MacroCommand = function () {
        return {
            commandList: [],
            add: function (command) {
                this.commandList.push(command);
            },
            execute: function () {
                for (var i = 0, command; command = this.commandList[i++];) {
                    command.execute();
                }
            }
        };
    };

    var openAcCommand = {
        execute: function () {
            console.log('打開空調');
        }
    };

    /*電視與音響鏈接在一塊兒,成爲一個組合對象(宏命令)*/
    var openTvCommand = {
        execute: function () {
            console.log('打開電視');
        }
    };
    var openSoundCommand = {
        execute: function () {
            console.log('打開音響');
        }
    };

    var macroCommand1 = MacroCommand();
    macroCommand1.add(openTvCommand);
    macroCommand1.add(openSoundCommand);

    /*關門,打開電腦,登QQ組成宏命令*/
    var closeDoorCommand = {
        execute: function () {
            console.log('關門')
        }
    };
    var openPcCommand = {
        execute: function () {
            console.log('開電腦');
        }
    };
    var openQQCommand = {
        execute: function () {
            console.log('上QQ');
        }
    };

    var macroCommand2 = MacroCommand();
    macroCommand2.add(closeDoorCommand);
    macroCommand2.add(openPcCommand);
    macroCommand2.add(openQQCommand);

    /*如今組合全部命令*/
    var macroCommand = MacroCommand();
    macroCommand.add(openAcCommand);
    macroCommand.add(macroCommand1);
    macroCommand.add(macroCommand2);

    /*最後給遙控器綁定超級命令*/
    var setCommand = (function (command) {
        document.getElementById('button').onclick = function () {
            command.execute();
        };
    })(macroCommand);
</script>
</body>
</html>

當按下遙控器的按鈕時,全部命令都將被依次執行,執行結果如圖所示:學習

從這個例子中能夠看到,基本對象能夠被組合成更復雜的組合對象,組合對象又能夠被組合,這樣不斷遞歸下去,這棵樹的結構能夠支持任意多的複雜度。在樹最終被構造完成以後,讓整棵樹最終運轉起來的步驟很是簡單,只須要調用最上層對象的execute方法。每當對最上層的對象進行一次請求時,其實是在對整個樹進行深度優先的搜索,而建立組合對象的程序員並不關心這些內在的細節,往這棵樹裏面添加一些新的節點對象是很是容易的事情。測試

5. 抽象類在組合模式中的做用

前面說到,組合模式最大的優勢在於能夠一致的對待組合對象和基本對象。客戶不須要知道當前處理的是宏命令仍是普通命令,只要它是一個命令,而且有execute方法,這個命令就能夠被添加到樹中。
這種透明性帶來的便利,在靜態語言中體現得尤其明顯。好比在Java中,實現組合模式的關鍵是composite類和Leaf類都必須繼承自一個Component抽象類。這個Component抽象類既表明組合對象,又表明葉對象,它也可以保證組合對象和葉對象擁有一樣的名字和方法,從而能夠對同一消息都作出反饋。組合對象和葉對象的具體類型被隱藏在Component抽象類身後。
針對Component抽象類來編寫程序,客戶操做的始終是Component對象,而不用去區分究竟是組合對象仍是葉對象。因此咱們往同一個對象裏的add方法裏,既能夠添加幾何對象,也能夠添加葉對象。
然而在JavaScript這種動態類型語言中,對象的多態性是與生俱來的,也沒有編譯器去檢查變量的類型,因此咱們一般不會去模擬一個「怪異」的抽象類,JavaScript中實現組合模式的難點在於要保證組合對象和葉對象擁有一樣的方法,這一般須要用鴨子類型的思想對它們進行接口檢查。
在JavaScript中實現組合模式,看起來缺少一些嚴謹性,咱們的代碼算不上安全,但能更快速和自由地開發,這既是JavaScript的缺點,也是它的優勢。

6. 透明性帶來的安全問題

組合模式的透明性使得發起請求的客戶不用去顧忌樹中的組合對象和葉對象的區別,但他們在本質上是有區別的。
組合對象能夠擁有子節點,葉對象下面就沒有子節點,因此咱們也許會發生一些誤操做,好比試圖往葉對象中添加子節點。解決方案一般是給葉對象也增長add方法,而且在調用這個方法時,拋出一個異常來及時提醒客戶,代碼以下:

var MacroCommand = function () {
        return {
            commandList: [],
            add: function (command) {
                this.commandList.push(command);
            },
            execute: function () {
                for (var i = 0, command; command = this.commandList[i++];) {
                    command.execute();
                }
            }
        };
    };

    var openTvCommand = {
        add: function () {
            throw new Error('葉對象不能添加子節點');
        },
        execute: function () {
            console.log('打開電視');
        }
    };
    var macroCommand = MacroCommand();
    macroCommand.add(openTvCommand);
    openTvCommand.add(macroCommand);    //Error: 葉對象不能添加子節點

7. 組合模式的例子——掃描文件夾

文件夾和文件之間的關係,很是適合用組合模式來描述。文件夾裏既能夠包含文件,又能夠包含其餘文件夾,最終可能組合成一棵樹。如今咱們來編寫代碼,首先分別定義好文件夾Folder和文件File這兩個類。

//Folder類
    var Folder = function (name) {
        this.name = name;
        this.files = [];
    };
    Folder.prototype.add = function (file) {
        this.files.push(file);
    };
    Folder.prototype.scan = function () {
        console.log('開始掃描文件夾:' + this.name);
        for (var i = 0, file; file = this.files[i++];) {
            file.scan();
        };
    };

    //File類
    var File = function (name) {
        this.name = name;
    };
    File.prototype.add = function () {
        throw new Error('文件下面不能再添加文件');
    };
    File.prototype.scan = function () {
        console.log('開始掃描文件:' + this.name);
    };

    //建立3個文件夾
    var folder = new Folder('學習資料');
    var folder1 = new Folder('JavaScript');
    var folder2 = new Folder('JQuery');

    //建立3個文件
    var file1 = new File('JavaScript設計模式與開發實踐');
    var file2 = new File('鋒利的JQuery');
    var file3 = new File('編程人生');

    //把文件添加到文件夾或文件夾添加到另外一文件夾中
    folder1.add(file1);
    folder2.add(file2);

    folder.add(folder1);
    folder.add(folder2);
    folder.add(file3);

    //掃描文件夾
    folder.scan();

8. 一些值得注意的地方

在使用組合模式的時候,還有如下幾個值得咱們注意的地方。

  1. 組合模式不是父子關係
    組合模式的樹型結構容易讓人誤覺得組合對象和葉對象的父子關係,這是不正確的。
    組合模式是一種HAS-A(聚合)的關係,而不是IS-A。組合對象包含一組葉對象,但Leaf(葉對象)並非Composite(組合對象)的子類。組合對象把請求委託給它所包含的全部葉對象,它們可以合做的關鍵是擁有相同的接口。
  2. 對葉對象操做的一致性
    組合模式除了要求組合對象和葉對象擁有相同的接口以外,還有一個必要的條件,就是對一組葉對象的操做必須具備一致性。
    好比公司要給全體員工發放元旦的過節費1000元,這個場景能夠運用組合模式,但若是公司給今天過生日的員工發送一封生日祝福的郵件,組合模式在這裏就沒有用武之地了,除非先把今天過生日的員工挑選出來。只有用一致的方式對待列表中的每一個葉對象的時候,才適合使用組合模式。
  3. 雙向映射關係
    發放過節費的通知步驟是從公司到各個部門,再到各個小組,最後到每一個員工的郵箱裏。這自己是一個組合模式的例子,但要考慮的一種狀況是,也許某些員工屬於多個組織架構。好比某位架構師既隸屬於開發組,又隸屬於架構組,對象之間的關係並非嚴格意義上的層次結構,在這種狀況下,是適不適合使用組合模式的,該架構師極可能會受到兩份過節費。
    這種複合狀況下咱們必須給父節點和子節點創建雙向映射關係,一個簡單的方法是給小組和員工對象都增長集合來保存對象的引用。可是這種相互間的引用至關複雜,並且對象之間產生了過多的耦合性,修改或刪除一個對象都變得困難,此時咱們能夠引入中介者模式來管理這些對象。
  4. 用職責鏈模式提升組合模式性能
    在組合模式中,若是樹的結構比較複雜,節點數量不少,在遍歷樹的過程當中,性能方面也許表現得不夠理想。有時候咱們確實能夠藉助一些技巧,在實際操做中避免遍歷整棵樹,有一種現成的方案是藉助職責鏈模式。職責鏈模式通常須要咱們手動去設置 鏈條,但在組合模式中,父對象和子對象之間實際上造成了自然的職責鏈。讓請求順着鏈條從父對象往子對象傳遞,或者是反過來從子對象往父對象傳遞,直到遇到能夠處理該請求的對象爲止,這也是職責鏈模式的經典運用場景之一。

9. 引用父對象

有時候咱們須要在子節點上保持對父節點的引用,好比在組合模式中使用職責鏈時,有可能須要讓請求從子節點往父節點上冒泡傳遞。還有當咱們刪除某個文件的時候,其實是從這個文件所在的上層文件夾中刪除該文件的。
如今來改寫掃描文件夾的代碼,使得在掃描整個文件夾以前,咱們能夠先移除某一個具體的文件。
首先改寫Folder類和File類,在這兩個類的構造函數中,增長this.parent屬性,而且在調用add方法的時候,正確設置文件或者文件夾的父節點。

var Folder = function (name) {
        this.name = name;
        this.parent = null; //增長parent的屬性,構造時無值
        this.files = [];
    };
    Folder.prototype.add = function (file) {
        file.parent = this; //修改file.parent的值,this指向調用add方法的對象
        this.files.push(file);
    };
    Folder.prototype.scan = function () {
        console.log('開始掃描文件夾:' + this.name);
        for (var i = 0, file, files = this.files; file = files[i++];) {
            file.scan();
        }
    };

接下來增長Folder.prototype.remove方法,表示移除該文件夾:

Folder.prototype.remove = function () {
        if (!this.parent) { //根節點或者樹外的遊離節點
            return;
        }
        //遍歷父節點中的files,假如等於當前this所指向的對象,則從父節點中刪除
        for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
            var file = files[l];
            if (file === this) {
                files.splice(l, 1);
            }
        }
    };

File類的實現基本一致:

var File = function (name) {
        this.parent = null;
        this.name = name;
    };
    File.prototype.add = function () {
        throw new Error('文件下面不能再添加文件');
    };
    File.prototype.scan = function () {
        console.log('開始掃描文件:' + this.name);
    };
    File.prototype.remove = function () {
        if (!this.parent) {
            return;
        }
        for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
            var file = files[l];
            if (file === this) {
                files.splice(l, 1);
            }
        }
    };

下面測試一個咱們的移除文件功能:

//建立3個文件夾
    var folder = new Folder('學習資料');
    var folder1 = new Folder('JavaScript');
    var folder2 = new Folder('JQuery');

    //建立3個文件
    var file1 = new File('JavaScript設計模式與開發實踐');
    var file2 = new File('鋒利的JQuery');
    var file3 = new File('編程人生');

    //把文件添加到文件夾或文件夾添加到另外一文件夾中
    folder1.add(file1);
    folder2.add(file2);

    folder.add(folder1);
    folder.add(folder2);
    folder.add(file3);

    //刪除文件夾
    folder1.remove();
    folder2.remove();

    //掃描文件夾
    folder.scan();

執行結果如圖所示:

10.什麼時候使用組合模式

組合模式若是運用得當,能夠大大簡化客戶的代碼。通常來講,組合模式適用於如下這兩種狀況。

  • 表示對象的部分——總體層次結構。組合模式能夠方便地構造一棵樹來表示對象的部分——總體結構。特別是咱們在開發期間不肯定這棵樹到底存在多少層次的時候。在樹的構造最終完成以後,只須要經過請求樹的最頂層對象,便能對整棵樹作統一的操做。在組合模式中增長和刪除樹的節點很是方便,而且符合開發——封閉原則。
  • 客戶但願統一對待樹中的全部對象。組合模式使客戶能夠忽略組合對象和葉對象的區別,客戶在面對這棵樹的時候,不用關心當前正在處理的對象是組合對象仍是葉對象,也就是不用寫一堆if,else語句來分別處理它們。組合對象和葉對象會各自作本身正確的事情,這是組合模式最重要的能力。

11. 小結

本章咱們瞭解了組合模式在JavaScript開發中的應用。組合模式可讓咱們使用樹型方式建立對象的結構。咱們能夠把相同的操做應用在組合對象和單個對象上。在大多數狀況下,咱們均可以忽略掉組合對象和單個對象之間的差異,從而用一致的方式來處理它們。
然而,組合模式並非完美的,它可能會產生一個這樣的系統:系統中的每一個對象看起來都與其餘對象差很少。它們的區別只有在運行的時候纔會顯現出來,這會使代碼難以理解。此外,若是經過組合模式建立了太多的對象,那麼這些對象可能會讓系統負擔不起。


參考書目:《JavaScript設計模式與開發實踐》

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息