JavaScript 设计模式(中)——7.组合模式

7 组合模式

组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的;

7.1 组合模式的用途

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构,如宏命令的例子,通过遍历该树形结构,调用组合对象的 execute 方法,程序会递归调用组合对象下面的叶对象的 execute 方法;组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性,只要约定对象上拥有可执行的 execute 方法即可;

7.2 请求在树中传递的过程

以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。总之,如果子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象,请求会继续往下传递。叶对象下面不会再有其他子节点,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还会有子节点;

_宏命令例子_1576393930_5178.png

7.3 透明性带来的安全问题

组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在本质上有是区别的,组合对象可以拥有子节点,叶对象下面就没有子节点,解决方案通常是给叶对象也增加 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 ) // Uncaught Error: 叶对象不能添加子节点

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 );
};
// 创建一些文件夹和文件对象, 并且让它们组合成一棵树
var folder = new Folder( '测试文件夹' );
var file = new File( 'JavaScript 设计模式与开发实践' );
folder.add( file );
// 操作树的最顶端对象,进行扫描整个文件夹的操作
folder.scan();

7.5 组合模式的注意点

  1. 组合模式不是父子关系:组合模式是一种 HAS-A(聚合)的关系,而不是 IS-A。组合对象包含一组叶对象,但 Leaf 并不是 Composite 的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键是拥有相同的接口;
  2. 对叶对象操作的一致性:组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性;
  3. 双向映射关系:假如存在叶对象处在多个组合对象的情况,那么在调用的时候,该叶对象的命令会执行多次,这种复合情况下必须给父节点和子节点建立双向映射关系,一个简单的方法是给组合对象和叶对象都增加集合来保存对方的引用。但这种相互间的引用相当复杂,而且对象之间产生了过多的耦合性,修改或者删除一个对象都变得困难,此时可以引入中介者模式来管理这些对象;
  4. 用职责链模式提高组合模式性能:在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现得不够理想,在实际操作中避免遍历整棵树,借助职责链模式进行解决,职责链模式一般需要手动去设置链条,但在组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运用场景之一。

7.6 引用父对象

之前示例中组合模式的树结构是从上至下的,但有时候需要在子节点上保持对父节点的引用,比如在组合模式中
使用职责链时,有可能需要让请求从子节点往父节点上冒泡传递。还有当删除某个文件的时候,实际上是从这个文件所在的上层文件夹中删除该文件的。

实例:改写扫描文件夹的代码,增加删除功能

// 改写 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 = 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();

7.7 使用组合模式场景

  • 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则
  • 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if、 else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

7.8 组合模式小结

组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们;但在使用了组合模式的系统中,每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解,并且组合模式会创建了太多的对象;

系列链接

  1. JavaScript 设计模式(上)——基础知识
  2. JavaScript 设计模式(中)——1.单例模式
  3. JavaScript 设计模式(中)——2.策略模式
  4. JavaScript 设计模式(中)——3.代理模式
  5. JavaScript 设计模式(中)——4.迭代器模式
  6. JavaScript 设计模式(中)——5.发布订阅模式
  7. JavaScript 设计模式(中)——6.命令模式
  8. JavaScript 设计模式(中)——7.组合模式
  9. JavaScript 设计模式(中)——8.模板方法模式
  10. JavaScript 设计模式(中)——9.享元模式
  11. JavaScript 设计模式(中)——10.职责链模式
  12. JavaScript 设计模式(中)——11. 中介者模式
  13. JavaScript 设计模式(中)——12. 装饰者模式
  14. JavaScript 设计模式(中)——13.状态模式
  15. JavaScript 设计模式(中)——14.适配器模式
  16. JavaScript 设计模式(下)——设计原则
  17. JavaScript 设计模式练习代码

本文主要参考了《JavaScript设计模式和开发实践》一书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容