《JavaScript設計模式與開發實踐》模式篇(7)—— 組合模式
組合模式將物件組合成樹形結構,以表示“部分-整體”的層次結構。 除了用來表示樹形結構之外,組合模式的另一個好處是通過物件的多型性表現,使得使用者對單個物件和組合物件的使用具有一致性
以命令模式中的巨集命令程式碼為例,巨集命令物件包含了一組具體的子命令物件,不管是巨集命令物件,還是子命令物件,都有一個execute方法負責執行命令。巨集命令中包含了一組子命令,它們組成了一個樹形結構,這裡是一棵結構非常簡單的樹
在組合模式中,請求在樹中傳遞的過程總是遵循一種邏輯。請求從樹最頂端的物件往下傳遞,如果當前處理請求的物件是葉物件(普通子命令),葉物件自身會對請求作出相應的處理;如果當前處理請求的物件是組合物件(巨集命令), 組合物件則會遍歷它屬下的子節點,將請求繼續傳遞給這些子節點。
- 組合模式下的巨集命令 目前的萬能遙控器,包含了關門、開電腦、登入 QQ 這 3 個命令。現在我們需要一個“超級萬能遙控器”,可以控制家裡所有的電器,這個遙控器擁有以下功能
- 開啟空調
- 開啟電視和音響
- 關門、開電腦、登入 QQ
var MacroCommand = function(){ return { commandsList: [], add: function( command ){ this.commandsList.push( command ); }, execute: function(){ for ( var i = 0, command; command = this.commandsList[ 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 ); 複製程式碼
從這個例子中可以看到,基本物件可以被組合成更復雜的組合物件,組合物件又可以被組合, 這樣不斷遞迴下去,這棵樹的結構可以支援任意多的複雜度。在樹最終被構造完成之後,讓整顆 樹最終運轉起來的步驟非常簡單,只需要呼叫最上層物件的 execute 方法。每當對最上層的物件 進行一次請求時,實際上是在對整個樹進行深度優先的搜尋,而建立組合物件的程式員並不關心這些內在的細節,往這棵樹裡面新增一些新的節點物件是非常容易的事情。
應用場景 —— 掃描資料夾
資料夾和檔案之間的關係,非常適合用組合模式來描述。資料夾裡既可以包含檔案,又可以 包含其他資料夾,最終可能組合成一棵樹 當使用用防毒軟體掃描該資料夾時,往往不會關心裡面有多少檔案和子資料夾,組合模式使得我們只需要操作最外層的資料夾進行掃描。
程式碼實現
/* 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, files = this.files; file = files[ i++ ]; ){ files 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 ); }; /*建立一些資料夾和檔案物件, 並且讓它們組合成一棵樹,這棵樹就是我們 F 盤裡的 現有檔案目錄結構*/ var folder = new Folder( '學習資料' ); var folder1 = new Folder( 'JavaScript/">JavaScript' ); var folder2 = new Folder ( 'jQuery' ); 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 ); /*現在的需求是把移動硬盤裡的檔案和資料夾都複製到這棵樹中,假設我們已經得到了這些檔案物件*/ var folder3 = new Folder( 'Nodejs' ); var file4 = new File( '深入淺出 Node.js' ); folder3.add( file4 ); var file5 = new File( 'JavaScript 語言精髓與程式設計實踐' ); /*接下來就是把這些檔案都新增到原有的樹中*/ folder.add( folder3 ); folder.add( file5 ); 複製程式碼
小結
組合模式可以讓我們使用樹形方式創 建物件的結構。我們可以把相同的操作應用在組合物件和單個物件上。在大多數情況下,我們都 可以忽略掉組合物件和單個物件之間的差別,從而用一致的方式來處理它們。 然而,組合模式並不是完美的,它可能會產生一個這樣的系統:系統中的每個物件看起來都 與其他物件差不多。它們的區別只有在執行的時候會才會顯現出來,這會使程式碼難以理解。此外,如果通過組合模式建立了太多的物件,那麼這些物件可能會讓系統負擔不起。