7 組合模式
組合模式就是用小的子對(duì)象來構(gòu)建更大的對(duì)象,而這些小的子對(duì)象本身也許是由更小的“孫對(duì)象”構(gòu)成的;
7.1 組合模式的用途
組合模式將對(duì)象組合成樹形結(jié)構(gòu),以表示“部分-整體”的層次結(jié)構(gòu),如宏命令的例子,通過遍歷該樹形結(jié)構(gòu),調(diào)用組合對(duì)象的 execute 方法,程序會(huì)遞歸調(diào)用組合對(duì)象下面的葉對(duì)象的 execute 方法;組合模式的另一個(gè)好處是通過對(duì)象的多態(tài)性表現(xiàn),使得用戶對(duì)單個(gè)對(duì)象和組合對(duì)象的使用具有一致性,只要約定對(duì)象上擁有可執(zhí)行的 execute 方法即可;
7.2 請(qǐng)求在樹中傳遞的過程
以宏命令為例,請(qǐng)求從樹最頂端的對(duì)象往下傳遞,如果當(dāng)前處理請(qǐng)求的對(duì)象是葉對(duì)象(普通子命令),葉對(duì)象自身會(huì)對(duì)請(qǐng)求作出相應(yīng)的處理;如果當(dāng)前處理請(qǐng)求的對(duì)象是組合對(duì)象(宏命令),組合對(duì)象則會(huì)遍歷它屬下的子節(jié)點(diǎn),將請(qǐng)求繼續(xù)傳遞給這些子節(jié)點(diǎn)??傊?,如果子節(jié)點(diǎn)是葉對(duì)象,葉對(duì)象自身會(huì)處理這個(gè)請(qǐng)求,而如果子節(jié)點(diǎn)還是組合對(duì)象,請(qǐng)求會(huì)繼續(xù)往下傳遞。葉對(duì)象下面不會(huì)再有其他子節(jié)點(diǎn),一個(gè)葉對(duì)象就是樹的這條枝葉的盡頭,組合對(duì)象下面可能還會(huì)有子節(jié)點(diǎn);

7.3 透明性帶來的安全問題
組合模式的透明性使得發(fā)起請(qǐng)求的客戶不用去顧忌樹中組合對(duì)象和葉對(duì)象的區(qū)別,但它們?cè)诒举|(zhì)上有是區(qū)別的,組合對(duì)象可以擁有子節(jié)點(diǎn),葉對(duì)象下面就沒有子節(jié)點(diǎn),解決方案通常是給葉對(duì)象也增加 add 方法,并且在調(diào)用這個(gè)方法時(shí),拋出一個(gè)異常來及時(shí)提醒客戶:
// 組合對(duì)象
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();
}
}
}
};
// 葉對(duì)象
var openTvCommand = {
execute: function(){ console.log( '打開電視' ); },
add: function(){
throw new Error( '葉對(duì)象不能添加子節(jié)點(diǎn)' );
}
};
var macroCommand = MacroCommand();
macroCommand.add( openTvCommand );
openTvCommand.add( macroCommand ) // Uncaught Error: 葉對(duì)象不能添加子節(jié)點(diǎn)
7.4 組合模式的例子——掃描文件夾
/******************************* 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++ ]; ){
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 );
};
// 創(chuàng)建一些文件夾和文件對(duì)象, 并且讓它們組合成一棵樹
var folder = new Folder( '測(cè)試文件夾' );
var file = new File( 'JavaScript 設(shè)計(jì)模式與開發(fā)實(shí)踐' );
folder.add( file );
// 操作樹的最頂端對(duì)象,進(jìn)行掃描整個(gè)文件夾的操作
folder.scan();
7.5 組合模式的注意點(diǎn)
- 組合模式不是父子關(guān)系:組合模式是一種 HAS-A(聚合)的關(guān)系,而不是 IS-A。組合對(duì)象包含一組葉對(duì)象,但 Leaf 并不是 Composite 的子類。組合對(duì)象把請(qǐng)求委托給它所包含的所有葉對(duì)象,它們能夠合作的關(guān)鍵是擁有相同的接口;
- 對(duì)葉對(duì)象操作的一致性:組合模式除了要求組合對(duì)象和葉對(duì)象擁有相同的接口之外,還有一個(gè)必要條件,就是對(duì)一組葉對(duì)象的操作必須具有一致性;
- 雙向映射關(guān)系:假如存在葉對(duì)象處在多個(gè)組合對(duì)象的情況,那么在調(diào)用的時(shí)候,該葉對(duì)象的命令會(huì)執(zhí)行多次,這種復(fù)合情況下必須給父節(jié)點(diǎn)和子節(jié)點(diǎn)建立雙向映射關(guān)系,一個(gè)簡(jiǎn)單的方法是給組合對(duì)象和葉對(duì)象都增加集合來保存對(duì)方的引用。但這種相互間的引用相當(dāng)復(fù)雜,而且對(duì)象之間產(chǎn)生了過多的耦合性,修改或者刪除一個(gè)對(duì)象都變得困難,此時(shí)可以引入中介者模式來管理這些對(duì)象;
- 用職責(zé)鏈模式提高組合模式性能:在組合模式中,如果樹的結(jié)構(gòu)比較復(fù)雜,節(jié)點(diǎn)數(shù)量很多,在遍歷樹的過程中,性能方面也許表現(xiàn)得不夠理想,在實(shí)際操作中避免遍歷整棵樹,借助職責(zé)鏈模式進(jìn)行解決,職責(zé)鏈模式一般需要手動(dòng)去設(shè)置鏈條,但在組合模式中,父對(duì)象和子對(duì)象之間實(shí)際上形成了天然的職責(zé)鏈。讓請(qǐng)求順著鏈條從父對(duì)象往子對(duì)象傳遞,或者是反過來從子對(duì)象往父對(duì)象傳遞,直到遇到可以處理該請(qǐng)求的對(duì)象為止,這也是職責(zé)鏈模式的經(jīng)典運(yùn)用場(chǎng)景之一。
7.6 引用父對(duì)象
之前示例中組合模式的樹結(jié)構(gòu)是從上至下的,但有時(shí)候需要在子節(jié)點(diǎn)上保持對(duì)父節(jié)點(diǎn)的引用,比如在組合模式中
使用職責(zé)鏈時(shí),有可能需要讓請(qǐng)求從子節(jié)點(diǎn)往父節(jié)點(diǎn)上冒泡傳遞。還有當(dāng)刪除某個(gè)文件的時(shí)候,實(shí)際上是從這個(gè)文件所在的上層文件夾中刪除該文件的。
實(shí)例:改寫掃描文件夾的代碼,增加刪除功能
// 改寫 Folder 類和 File 類,在這兩個(gè)類的構(gòu)造函數(shù)中增加 this.parent 屬性,并且在調(diào)用 add 方法的時(shí)候,正確設(shè)置文件或者文件夾的父節(jié)點(diǎn):
var Folder = function( name ){
this.name = name;
this.parent = null; // 增加 this.parent 屬性
this.files = [];
};
Folder.prototype.add = function( file ){
file.parent = this; //設(shè)置父對(duì)象
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 ){ // 根節(jié)點(diǎn)或者樹外的游離節(jié)點(diǎn)
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 ); }
}
};
// File 類的實(shí)現(xiàn)基本一致:
var File = function( name ){
this.name = name;
this.parent = null;
};
File.prototype.add = function(){ throw new Error( '不能添加在文件下面' ); };
File.prototype.scan = function(){ console.log( '開始掃描文件: ' + this.name ); };
File.prototype.remove = function(){
if ( !this.parent ){ // 根節(jié)點(diǎn)或者樹外的游離節(jié)點(diǎn)
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 ); }
}
};
// 測(cè)試一下移除文件功能:
var folder = new Folder( '學(xué)習(xí)資料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new Folder ( '深入淺出 Node.js' );
folder1.add( new File( 'JavaScript 設(shè)計(jì)模式與開發(fā)實(shí)踐' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除文件夾
folder.scan();
7.7 使用組合模式場(chǎng)景
- 表示對(duì)象的部分-整體層次結(jié)構(gòu)。組合模式可以方便地構(gòu)造一棵樹來表示對(duì)象的部分-整體結(jié)構(gòu)。特別是在開發(fā)期間不確定這棵樹到底存在多少層次的時(shí)候。在樹的構(gòu)造最終完成之后,只需要通過請(qǐng)求樹的最頂層對(duì)象,便能對(duì)整棵樹做統(tǒng)一的操作。在組合模式中增加和刪除樹的節(jié)點(diǎn)非常方便,并且符合開放-封閉原則。
- 客戶希望統(tǒng)一對(duì)待樹中的所有對(duì)象。組合模式使客戶可以忽略組合對(duì)象和葉對(duì)象的區(qū)別,客戶在面對(duì)這棵樹的時(shí)候,不用關(guān)心當(dāng)前正在處理的對(duì)象是組合對(duì)象還是葉對(duì)象,也就不用寫一堆 if、 else 語(yǔ)句來分別處理它們。組合對(duì)象和葉對(duì)象會(huì)各自做自己正確的事情,這是組合模式最重要的能力。
7.8 組合模式小結(jié)
組合模式可以讓我們使用樹形方式創(chuàng)建對(duì)象的結(jié)構(gòu)。我們可以把相同的操作應(yīng)用在組合對(duì)象和單個(gè)對(duì)象上。在大多數(shù)情況下都可以忽略掉組合對(duì)象和單個(gè)對(duì)象之間的差別,從而用一致的方式來處理它們;但在使用了組合模式的系統(tǒng)中,每個(gè)對(duì)象看起來都與其他對(duì)象差不多。它們的區(qū)別只有在運(yùn)行的時(shí)候會(huì)才會(huì)顯現(xiàn)出來,這會(huì)使代碼難以理解,并且組合模式會(huì)創(chuàng)建了太多的對(duì)象;
系列鏈接
- JavaScript 設(shè)計(jì)模式(上)——基礎(chǔ)知識(shí)
- JavaScript 設(shè)計(jì)模式(中)——1.單例模式
- JavaScript 設(shè)計(jì)模式(中)——2.策略模式
- JavaScript 設(shè)計(jì)模式(中)——3.代理模式
- JavaScript 設(shè)計(jì)模式(中)——4.迭代器模式
- JavaScript 設(shè)計(jì)模式(中)——5.發(fā)布訂閱模式
- JavaScript 設(shè)計(jì)模式(中)——6.命令模式
- JavaScript 設(shè)計(jì)模式(中)——7.組合模式
- JavaScript 設(shè)計(jì)模式(中)——8.模板方法模式
- JavaScript 設(shè)計(jì)模式(中)——9.享元模式
- JavaScript 設(shè)計(jì)模式(中)——10.職責(zé)鏈模式
- JavaScript 設(shè)計(jì)模式(中)——11. 中介者模式
- JavaScript 設(shè)計(jì)模式(中)——12. 裝飾者模式
- JavaScript 設(shè)計(jì)模式(中)——13.狀態(tài)模式
- JavaScript 設(shè)計(jì)模式(中)——14.適配器模式
- JavaScript 設(shè)計(jì)模式(下)——設(shè)計(jì)原則
- JavaScript 設(shè)計(jì)模式練習(xí)代碼