JavaScript 設(shè)計(jì)模式(中)——7.組合模式

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);

_宏命令例子_1576393930_5178.png

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)

  1. 組合模式不是父子關(guān)系:組合模式是一種 HAS-A(聚合)的關(guān)系,而不是 IS-A。組合對(duì)象包含一組葉對(duì)象,但 Leaf 并不是 Composite 的子類。組合對(duì)象把請(qǐng)求委托給它所包含的所有葉對(duì)象,它們能夠合作的關(guān)鍵是擁有相同的接口;
  2. 對(duì)葉對(duì)象操作的一致性:組合模式除了要求組合對(duì)象和葉對(duì)象擁有相同的接口之外,還有一個(gè)必要條件,就是對(duì)一組葉對(duì)象的操作必須具有一致性;
  3. 雙向映射關(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ì)象;
  4. 用職責(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ì)象;

系列鏈接

  1. JavaScript 設(shè)計(jì)模式(上)——基礎(chǔ)知識(shí)
  2. JavaScript 設(shè)計(jì)模式(中)——1.單例模式
  3. JavaScript 設(shè)計(jì)模式(中)——2.策略模式
  4. JavaScript 設(shè)計(jì)模式(中)——3.代理模式
  5. JavaScript 設(shè)計(jì)模式(中)——4.迭代器模式
  6. JavaScript 設(shè)計(jì)模式(中)——5.發(fā)布訂閱模式
  7. JavaScript 設(shè)計(jì)模式(中)——6.命令模式
  8. JavaScript 設(shè)計(jì)模式(中)——7.組合模式
  9. JavaScript 設(shè)計(jì)模式(中)——8.模板方法模式
  10. JavaScript 設(shè)計(jì)模式(中)——9.享元模式
  11. JavaScript 設(shè)計(jì)模式(中)——10.職責(zé)鏈模式
  12. JavaScript 設(shè)計(jì)模式(中)——11. 中介者模式
  13. JavaScript 設(shè)計(jì)模式(中)——12. 裝飾者模式
  14. JavaScript 設(shè)計(jì)模式(中)——13.狀態(tài)模式
  15. JavaScript 設(shè)計(jì)模式(中)——14.適配器模式
  16. JavaScript 設(shè)計(jì)模式(下)——設(shè)計(jì)原則
  17. JavaScript 設(shè)計(jì)模式練習(xí)代碼

本文主要參考了《JavaScript設(shè)計(jì)模式和開發(fā)實(shí)踐》一書

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容