組合模式將物件組合成樹形結構,以表示“部分-整體”的層次結構。 除了用來表示樹形結構之外,組合模式的另一個好處是通過物件的多型性表現,使得使用者對單個物件和組合物件的使用具有一致性
以命令模式中的巨集命令程式碼為例,巨集命令物件包含了一組具體的子命令物件,不管是巨集命令物件,還是子命令物件,都有一個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` );
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 );
複製程式碼
小結
組合模式可以讓我們使用樹形方式創 建物件的結構。我們可以把相同的操作應用在組合物件和單個物件上。在大多數情況下,我們都 可以忽略掉組合物件和單個物件之間的差別,從而用一致的方式來處理它們。
然而,組合模式並不是完美的,它可能會產生一個這樣的系統:系統中的每個物件看起來都 與其他物件差不多。它們的區別只有在執行的時候會才會顯現出來,這會使程式碼難以理解。此外,如果通過組合模式建立了太多的物件,那麼這些物件可能會讓系統負擔不起。