[1]宏指令
[2]掃描檔案夾
[3]注意事項
[4]引用父對象
前面的話
在程式設計中,有一些和“事物是由相似的子事物構成”類似的思想。組合模式就是用小的子對象來建構更大的對象,而這些小的子對象本身也許是由更小的“孫對象”構成的。本文将詳細介紹組合模式
宏指令
宏指令對象包含了一組具體的子指令對象,不管是宏指令對象,還是子指令對象,都有一個execute方法負責執行指令。現在回顧一下指令模式中關于萬能遙控器的宏指令代碼:
var closeDoorCommand = {
execute: function(){
console.log( '關門' );
}
};
var openPcCommand = {
execute: function(){
console.log( '開電腦' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登入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 macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();
宏指令中包含了一組子指令,它們組成了一個樹形結構,這裡是一棵結構非常簡單的樹

其中,marcoCommand被稱為組合對象,closeDoorCommand、openPcCommand、openQQCommand都是子對象。在macroCommand的execute方法裡,并不執行真正的操作,而是周遊它所包含的子對象,把真正的execute請求委托給這些子對象。macroCommand表現得像一個指令,但它實際上隻是一組真正指令的“代理”。并非真正的代理,雖然結構上相似,但macroCommand隻負責傳遞請求給子對象,它的目的不在于控制對子對象的通路
組合模式将對象組合成樹形結構,以表示“部分——整體”的層次結構。除了用來表示樹形結構之外,組合模式的另一個好處是通過對象的多态性表現,使得使用者對單個對象群組合對象的使用具有一緻性
1、表示樹形結構。組合模式的優點:提供了一種周遊樹形結構的方案,通過調用組合對象的execute方法,程式會遞歸調用組合對象下面的子對象的execute方法。組合模式可以非常友善地描述對象部分——整體層次結構
2、利用對象多态性統一對待組合對象和單個對象。利用對象的多态性表現,可以使用戶端忽略組合對象和單個對象的不同。在組合模式中,客戶将統一地使用組合結構中的所有對象,而不需要關心它究竟是組合對象還是單個對象
這在實際開發中會給客戶帶來相當大的便利性,往萬能遙控器裡面添加一個指令時,并不關心這個指令是宏指令還是普通子指令。隻需要确定它是一個指令,并且這個指令擁有可執行的execute方法,那麼這個指令就可以被添加
當宏指令和普通子指令接收到執行execute方法的請求時,宏指令和普通子指令都會做它們各自認為正确的事情。這些差異是隐藏在客戶背後的,在客戶看來,這種透明性可以非常自由地擴充程式
在組合模式中,請求在樹中傳遞的過程總是遵循一種邏輯。以宏指令為例,請求從樹最頂端的對象往下傳遞,如果目前處理請求的對象是子對象(普通子指令),子對象自身會對請求作出相應的處理;如果目前處理請求的對象是組合對象(宏指令),組合對象則會周遊它屬下的子節點,将請求繼續傳遞給這些子節點
總而言之,如果子節點是子對象,子對象自身會處理這個請求,而如果子節點還是組合對象,請求會繼續往下傳遞。子對象下面不會再有其他子節點,一個子對象就是樹的這條枝葉的盡頭,組合對象下面可能還會有子節點
請求從上到下沿着樹進行傳遞,直到樹的盡頭。作為客戶,隻需要關心樹最頂層的組合對象,客戶隻需要請求這個組合對象,請求便會沿着樹往下傳遞,依次到達所有的子對象
目前的萬能遙控器,包含了關門、開電腦、登入QQ這3個指令。現在需要一個“超級萬能遙控器”,可以控制家裡所有的電器,這個遙控器擁有以下功能:打開空調、打開電視和音響、關門、開電腦、登入QQ
首先在節點中放置一個按鈕button來表示這個超級萬能遙控器,超級萬能遙控器上安裝了一個宏指令,當執行這個宏指令時,會依次周遊執行它所包含的子指令,代碼如下:
<button id="button">按我</button>
<script>
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 );
</script>
當按下遙控器的按鈕時,所有指令都将被依次執行
基本對象可以被組合成更複雜的組合對象,組合對象又可以被組合,這樣不斷遞歸下去,這棵樹的結構可以支援任意多的複雜度。在樹最終被構造完成之後,讓整顆樹最終運轉起來的步驟非常簡單,隻需要調用最上層對象的execute方法。每當對最上層的對象進行一次請求時,實際上是在對整個樹進行深度優先的搜尋,而建立組合對象的程式員并不關心這些内在的細節,往這棵樹裡面添加一些新的節點對象是非常容易的事情
組合模式最大的優點在于可以一緻地對待組合對象和基本對象。客戶不需要知道目前處理的是宏指令還是普通指令,隻要它是一個指令,并且有execute方法,這個指令就可以被添加到樹中
在javascript這種動态類型語言中,對象的多态性是與生俱來的,也沒有編譯器去檢查變量的類型,javascript中實作組合模式的難點在于要保證組合對象和子對象對象擁有同樣的方法,這通常需要用鴨子類型的思想對它們進行接口檢查
在javascript中實作組合模式,看起來缺乏一些嚴謹性,代碼算不上安全,但能更快速和自由地開發,這既是javascript的缺點,也是它的優點
組合模式的透明性使得發起請求的客戶不用去顧忌樹中組合對象和基本對象的差別,但它們在本質上是有差別的
組合對象可以擁有子節點,基本對象下面就沒有子節點,是以也許會發生一些誤操作,比如試圖往基本對象中添加子節點。解決方案通常是給基本對象也增加add方法,并且在調用這個方法時,抛出一個異常來及時提醒客戶
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( '基本對象不能添加子節點' );
}
};
var macroCommand = MacroCommand();
macroCommand.add( openTvCommand );
openTvCommand.add( macroCommand );
掃描檔案夾
檔案夾和檔案之間的關系,非常适合用組合模式來描述。檔案夾裡既可以包含檔案,又可以包含其他檔案夾,最終可能組合成一棵樹,組合模式在檔案夾的應用中有以下兩層好處
1、組合模式讓Ctrl+V、Ctrl+C成為了一個統一的操作。例如,在移動硬碟裡找到了一些電子書,想把它們複制到F盤中的學習資料檔案夾。在複制這些電子書的時候,并不需要考慮這批檔案的類型,不管它們是單獨的電子書還是被放在了檔案夾中
2、用防毒軟體掃描該檔案夾時,往往不會關心裡面有多少檔案和子檔案夾,組合模式使得隻需要操作最外層的檔案夾進行掃描
現在來編寫代碼,首先分别定義好檔案夾Folder和檔案File這兩個類。見如下代碼:
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();
}
};
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 );
通過這個例子,再次看到客戶是如何同等對待組合對象和基本對象的。在添加一批檔案的操作過程中,客戶不用分辨它們到底是檔案還是檔案夾。新增加的檔案和檔案夾能夠很容易地添加到原來的樹結構中,和樹裡已有的對象一起工作。改變了樹的結構,增加了新的資料,卻不用修改任何一句原有的代碼,這是符合開放——封閉原則的
運用了組合模式之後,掃描整個檔案夾的操作也是輕而易舉的,隻需要操作樹的最頂端對象:
folder.scan();
注意事項
在使用組合模式的時候,還有以下幾個值得注意的地方
1、組合模式不是父子關系組合模式的樹型結構容易讓人誤以為組合對象和基本對象是父子關系。組合模式是一種HAS-A(聚合)的關系,而不是IS-A。組合對象包含一組基本對象,但Leaf并不是Composite的子類。組合對象把請求委托給它所包含的所有基本對象,它們能夠合作的關鍵是擁有相同的接口
2、對子對象操作的一緻性。組合模式除了要求組合對象和子對象擁有相同的接口之外,還有一個必要條件,就是對一組子對象的操作必須具有一緻性。比如公司要給全體員工發放元旦的過節費1000塊,這個場景可以運用組合模式,但如果公司給今天過生日的員工發送一封生日祝福的郵件,組合模式在這裡就沒有用武之地了,除非先把今天過生日的員工挑選出來。隻有用一緻的方式對待清單中的每個子對象的時候,才适合使用組合模式
3、雙向映射關系。發放過節費的通知步驟是從公司到各個部門,再到各個小組,最後到每個員工的郵箱裡。這本身是一個組合模式的好例子,但要考慮的一種情況是,也許某些員工屬于多個組織架構。比如某位架構師既隸屬于開發組,又隸屬于架構組,對象之間的關系并不是嚴格意義上的層次結構,在這種情況下,是不适合使用組合模式的,該架構師很可能會收到兩份過節費。這種複合情況下,必須給父節點和子節點建立雙向映射關系,一個簡單的方法是給小組和員工對象都增加集合來儲存對方的引用。但是這種互相間的引用相當複雜,而且對象之間産生了過多的耦合性,修改或者删除一個對象都變得困難,此時可以引入中介者模式來管理這些對象
4、用職責鍊模式提高組合模式性能。在組合模式中,如果樹的結構比較複雜,節點數量很多,在周遊樹的過程中,性能方面也許表現得不夠理想。有時候确實可以借助一些技巧,在實際操作中避免周遊整棵樹,有一種現成的方案是借助職責鍊模式。職責鍊模式一般需要手動去設定鍊條,但在組合模式中,父對象和子對象之間實際上形成了天然的職責鍊。讓請求順着鍊條從父對象往子對象傳遞,或者是反過來從子對象往父對象傳遞,直到遇到可以處理該請求的對象為止,這也是職責鍊模式的經典運用場景之一
引用父對象
組合對象儲存了它下面的子節點的引用,這是組合模式的特點,此時樹結構是從上至下的。但有時候需要在子節點上保持對父節點的引用,比如在組合模式中使用職責鍊時,有可能需要讓請求從子節點往父節點上冒泡傳遞。還有當删除某個檔案時,實際上是從這個檔案所在的上層檔案夾中删除該檔案的
現在來改寫掃描檔案夾的代碼,使得在掃描整個檔案夾之前,可以先移除某一個具體的檔案
首先改寫Folder類和File類,在這兩個類的構造函數中,增加this.parent屬性,并且在調用add方法的時候,正确設定檔案或者檔案夾的父節點:
var Folder = function( name ){
this.name = name;
this.parent = null; //增加this.parent 屬性
this.files = [];
};
Folder.prototype.add = function( file ){
file.parent = this; //設定父對象
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方法裡,首先會判斷this.parent,如果this.parent為null,那麼這個檔案夾要麼是樹的根節點,要麼是還沒有添加到樹的遊離節點,這時候沒有節點需要從樹中移除,暫且讓remove方法直接return,表示不做任何操作。如果this.parent不為null,則說明該檔案夾有父節點存在,此時周遊父節點中儲存的子節點清單,删除想要删除的子節點
Folder.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 );
}
}
};
File類的實作基本一緻:
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 ){ //根節點或者樹外的遊離節點
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( '學習資料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new Folder ( '深入淺出Node.js' );
folder1.add( new File( 'JavaScript 設計模式與開發實踐' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除檔案夾
folder.scan();
組合模式如果運用得當,可以大大簡化客戶的代碼。一般來說,組合模式适用于以下這兩種情況
1、表示對象的部分——整體層次結構。組合模式可以友善地構造一棵樹來表示對象的部分——整體結構。特别是在開發期間不确定這棵樹到底存在多少層次的時候。在樹的構造最終完成之後,隻需要通過請求樹的最頂層對象,便能對整棵樹做統一的操作。在組合模式中增加和删除樹的節點非常友善,并且符合開放——封閉原則
2、客戶希望統一對待樹中的所有對象。組合模式使客戶可以忽略組合對象和子對象的差別,客戶在面對這棵樹的時候,不用關心目前正在處理的對象是組合對象還是子對象,也就不用寫一堆if、else語句來分别處理它們。組合對象和子對象會各自做自己正确的事情,這是組合模式最重要的能力
然而,組合模式并不是完美的,它可能會産生一個這樣的系統:系統中的每個對象看起來都與其他對象差不多。它們的差別隻有在運作的時候會才會顯現出來,這會使代碼難以了解。此外,如果通過組合模式建立了太多的對象,那麼這些對象可能會讓系統負擔不起
好的代碼像粥一樣,都是用時間熬出來的