咱們在命令模式中講解過宏命令的結構和做用。宏命令對象包含了一組具體的子命令對象,無論是宏命令對象,仍是子命令對象,都有一個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只負責傳遞請求給葉對象,它的目的不在於控制對葉對象的訪問。編程
組合模式將對象組合成樹形結構,以表示「部分——總體」的層次結構。除了用來表示樹形結構以外,組合模式的另外一個好處是經過對象的多態性表現,使得用戶對單個對象和組合對象的使用具備一致性,下面分別說明。設計模式
這在實際開發中會給客戶帶來至關大的便利性,當咱們往萬能遙控器裏面添加一個命令的時候,並不關心這個命令是宏命令仍是普通子命令。這點對於咱們不重要,咱們只須要肯定它是一個命令,而且這個命令擁有可執行的execute方法,那麼這個命令就能夠被添加進萬能遙控器。
當宏命令和普通子命令接收到執行execute方法的請求時,宏命令和普通子命令都會作它們各自認爲正確的事情。這些差別是隱藏在客戶背後的,在客戶看來,這種透明性可讓咱們很是自由地擴展這個萬能遙控器。安全
在組合模式中,請求在樹中傳遞的過程老是遵循一種邏輯。
以宏命令爲例,請求從樹中最頂端的對象往下傳遞,若是當前處理請求的對象是葉對象(普通子命令),葉對象自身會對請求做出相應的處理:若是當前處理請求的對象是組合對象(宏命令),組合對象則會遍歷它屬下的子節點,將請求繼續傳遞給這些子節點。
總而言之,若是子節點是葉對象,葉對象自身會處理這個請求,而若是子節點仍是組合對象,請求會繼續往下傳遞。葉對象下面不會再有其它子節點,一個葉對象就是樹的這條枝葉的盡頭,組合對象下面可能還會有子節點,如圖所示:架構
請求從上到下沿着樹進行傳遞,直到樹的盡頭。做爲客戶,只須要關心樹最頂層的組合對象,客戶只須要請求這個組合對象,請求便會沿着樹往下傳遞,依次到達全部的葉對象。
在剛剛的例子中,因爲宏命令和子命令組成的樹太過簡單,咱們還不能清楚的看到組合模式帶來的好處,若是隻是簡單的遍歷一組子節點,迭代器便能解決全部的問題。接下來咱們將創造一個更強大的宏命令,這個宏命令中又包含了另一些宏命令和普通子命令,看起來是一顆相對較複雜的樹。函數
目前的萬能遙控器,包含了關門,開電腦,上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方法。每當對最上層的對象進行一次請求時,其實是在對整個樹進行深度優先的搜索,而建立組合對象的程序員並不關心這些內在的細節,往這棵樹裏面添加一些新的節點對象是很是容易的事情。測試
前面說到,組合模式最大的優勢在於能夠一致的對待組合對象和基本對象。客戶不須要知道當前處理的是宏命令仍是普通命令,只要它是一個命令,而且有execute方法,這個命令就能夠被添加到樹中。
這種透明性帶來的便利,在靜態語言中體現得尤其明顯。好比在Java中,實現組合模式的關鍵是composite類和Leaf類都必須繼承自一個Component抽象類。這個Component抽象類既表明組合對象,又表明葉對象,它也可以保證組合對象和葉對象擁有一樣的名字和方法,從而能夠對同一消息都作出反饋。組合對象和葉對象的具體類型被隱藏在Component抽象類身後。
針對Component抽象類來編寫程序,客戶操做的始終是Component對象,而不用去區分究竟是組合對象仍是葉對象。因此咱們往同一個對象裏的add方法裏,既能夠添加幾何對象,也能夠添加葉對象。
然而在JavaScript這種動態類型語言中,對象的多態性是與生俱來的,也沒有編譯器去檢查變量的類型,因此咱們一般不會去模擬一個「怪異」的抽象類,JavaScript中實現組合模式的難點在於要保證組合對象和葉對象擁有一樣的方法,這一般須要用鴨子類型的思想對它們進行接口檢查。
在JavaScript中實現組合模式,看起來缺少一些嚴謹性,咱們的代碼算不上安全,但能更快速和自由地開發,這既是JavaScript的缺點,也是它的優勢。
組合模式的透明性使得發起請求的客戶不用去顧忌樹中的組合對象和葉對象的區別,但他們在本質上是有區別的。
組合對象能夠擁有子節點,葉對象下面就沒有子節點,因此咱們也許會發生一些誤操做,好比試圖往葉對象中添加子節點。解決方案一般是給葉對象也增長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: 葉對象不能添加子節點
文件夾和文件之間的關係,很是適合用組合模式來描述。文件夾裏既能夠包含文件,又能夠包含其餘文件夾,最終可能組合成一棵樹。如今咱們來編寫代碼,首先分別定義好文件夾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();
在使用組合模式的時候,還有如下幾個值得咱們注意的地方。
有時候咱們須要在子節點上保持對父節點的引用,好比在組合模式中使用職責鏈時,有可能須要讓請求從子節點往父節點上冒泡傳遞。還有當咱們刪除某個文件的時候,其實是從這個文件所在的上層文件夾中刪除該文件的。
如今來改寫掃描文件夾的代碼,使得在掃描整個文件夾以前,咱們能夠先移除某一個具體的文件。
首先改寫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();
執行結果如圖所示:
組合模式若是運用得當,能夠大大簡化客戶的代碼。通常來講,組合模式適用於如下這兩種狀況。
本章咱們瞭解了組合模式在JavaScript開發中的應用。組合模式可讓咱們使用樹型方式建立對象的結構。咱們能夠把相同的操做應用在組合對象和單個對象上。在大多數狀況下,咱們均可以忽略掉組合對象和單個對象之間的差異,從而用一致的方式來處理它們。
然而,組合模式並非完美的,它可能會產生一個這樣的系統:系統中的每一個對象看起來都與其餘對象差很少。它們的區別只有在運行的時候纔會顯現出來,這會使代碼難以理解。此外,若是經過組合模式建立了太多的對象,那麼這些對象可能會讓系統負擔不起。
參考書目:《JavaScript設計模式與開發實踐》