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

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