热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【学习笔记javascript设计模式与开发实践(组合模式)10】

第10章组合模式在程序设计中,也有一些和“事物是由相似的子事物构成”类似的思想。组合模式就是肜小的子对象来构建更大的对象,而这些小的子对象本身也许是由

第10章 组合模式

在程序设计中,也有一些和“事物是由相似的子事物构成”类似的思想。组合模式就是肜小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。


10.1 回顾宏命令

我们在第9章命令模式中讲解过宏命令的结构和作用。宏命令对象包含了一组个体的子命令对象,不管是宏命令对象,还是子命令对象,都有一个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(vari=0,command;command=this.commandsList[i++];){command.execute();}}}
};varmacroCommand=MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();


通过观察这段代码,我们很容易发现,宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵非常简单的树。

其中,marcoCommand被称为组合对象,closeDoorCommandopenPcCommandopenQQCommand都是叶对象。在macroCommandexecute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象。

macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问。


10.2 组合模式的用途

组合模式将对象组合成树形结构,以表示“部分—整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用一致性,

 

l   表示树形结构。通过回顾上面的例子,我们很容易找到组合模式的一个优点:提供一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法,所以我们的万能遥控器只需要一次操作,便可以依次完成关门、打开电脑、登录QQ这几件事。组合模式可以非常方便地描述对象部分—整体层次结构

l   利用对象的多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。

这在实际开发中给客户带来相当大的方便,当我们往万能遥控器里面添加一个命令的时候,并不关心这个命令是宏命令还是普通子命令。这点对于我们不重要,我们只需要确定它是一个命令,并且这个命令拥有可执行的execute方法,那么这个命令就可以被添加进万能遥控器。

当宏命令和普通子命令接收到执行execute方法的请求时,宏命令和普通子命令都会做它们各自认为正确的事情。这些差异是隐藏在客户背后的,在客户看来,这种透明性可以让我们非常自由地扩展这个万能遥控器。


10.3 请求在树中传递的过程

在组合模式中,请示在树中传递过程总是遵循一种逻辑。

以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象,叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。

总而言之,如果子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象,请求会继续往下传递。叶对象下面不会再有其他子节点,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还会有子节点。请求从上到下没着树进行传递,直到树的尽头。


10.4 更强大的宏命令

目前的万能遥控器,包含了关门、开电脑、登录QQ这3个命令。现在我们需要一个“超级万能遥控器”可以控制家里所有的电器,这个遥控器拥有以下功能:

n   打开空调

n   打开电视和音响

n   关门、开电脑、登录QQ

首先在节点中放置一个按钮button来表示这个超级万能遥控器,超级万能遥控器上安装了一个宏命令,当执行宏命令时,会依次遍历执行它所包含的子命令:




从这个例子中可以看到,基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。在树最终被构造完成之后,让整颗树最终运转起来的步骤非常简单,只需要调用最上层对象的execute方法。而创建组合对象的程序员并不关心这些内在的细节,往这棵树里添加一些新的节点对象是非常容易的事情。


10.5 抽象类在组合模式中的作用

前端说到,组合模式最大的优点在于可以致地对待组合对象和基本对象。客户不需要知道当前处理的是宏命令还是普通命令,只要它是一个命令,并且有execute方法,这个命令就可以被添加到树中。

这种透明性带来的便利,在静态类型语言中体现得尤为明显。比如在java中,实现组合模式的关键是Composite类和Leaf类都必须继承自一个Compenent抽象类既代表组合对象,又代表叶子对象,它也能够保证组合对象和叶对象拥有同样的名字的方法,从而可以对同一消息都做出反馈。组合对象和叶对象的具体类型被隐藏在Compenent抽象类身后。

针对Compenent抽象类来编写程序,客户操作的始终是Compenent对象,而不用去区分到底是组合对象还是叶对象。所以我们往同一个对象里的add方法,即可以添加组合对象,也可以添加叶对象如下:

 

java代码:

public abstract class Component {public void add(Compenent child){}public void remove(Compenent child){}
}public class Composite extends Component {public void add(Componentchild){};public voidremove(Component child){};
}public class Leaf extends Component{public void add(Component child){throw new UnsupportedOperationException() //叶子节点不能添加子节点}public void remove(Component child){}}

}public class client(){public static void main(String args[]){Component root = new Composite();Component c1 = new Composite();Component c2 = new Composite();Component leaf1 = new Leaf();Component leaf12= new Leaf();root.add(c1);root.add(c2);c1.add(leaf1);c2.add(leaf2);}
}


然而在Javascript这种动态类型语言中,对象的多态性是与生俱来的,也没有编译器检查变量的类型,所以我们通常不会去模拟一个“怪异”的抽象类,Javascript中实现组合模式的难点在于要保证组合对象和叶子对象拥有相同的方法,这通常需要用鸭子类型的思想对它们进行接口检查。

在js中实现组合模式,看起来缺乏一些严谨性,我们的代码算不上安全,但能更快速和自由地开发,这既是Javascript的缺点,也是它的优点。


10.6 透明性带来的安全问题

组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在本质上是有区别的。

组合对象可以拥有子节点、叶对象下面就没有子节点,所以我们也许会发生一些误操作,比如试图往叶子中添加子节点。解决方案通常是给叶子对象也增加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); //throwerror


 


10.7 组合模式的例子---扫描文件夹

文件和文件夹之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树,组合模式在文件夹的应用中有以下两层好处。

n   例如,我在同事的移动硬盘里找到一些电子书,我想把它们复制到F盘中的学习资料文件夹。在复制这些电子书的时候,我并不需要考虑这批文件的类型,不管它们是单独的电子书还是被放在了文件夹中。组合模式让Ctrl+V、Ctrl+C成为了一个统一的操作。

n   当我用杀毒软件扫描文件夹时。往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹时行扫描。

现在我们来编写代码,首先分别定义好文件夹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();}
}/***** File ******/
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(‘Node.js’);

var file4 = new File(‘深入浅出Node.js’);

var file5 = new File(‘Javascript语言精髓与编辑实践’);

接下来把这些文件添加到原有的树中:

folder.add(folder3);

folder.add(file5);

通过这个例子,我们再次看到客户是如何对待组合对象和叶对象。在添加一批文件的操作过程中,客户不用分辩它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树中已有的对象一起工作。


10.8 一些值得注意的地方

在使用组合模式的时候,还有以下几个值得我们注意的地方。

1.    组合模式的树型结构容易让人误以为组合对象和叶对象是父子关系,这是不正确的。

组合模式就是一种HAS-A的关系。组合对象包含一组叶对象,但Leaf并不是Composite的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作关键是拥有相同的接口。

2.    对叶对象操作的一致性

组合模式除了要示组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性。

比如公司要给全体员工发放元旦的过节费1000块,这个场景可以运用组合模式,但如果公司给今天过生日的员工发送一封生日祝福的邮件,组合模式就没有用武之地了,除非先把今天过生日的员工挑选出来。只用一致的方式对待列表中的每个叶子对象的时候,才适合使用组合模式。

3.    双向映射关系

发放过节费的通知步骤是从公司到各个部门,再到各个小组,最后到每个员工的邮箱里。这本身是一个组合模式的好例子,但要考虑的一种情况是,也许某些员工属于多个组织架构。比如某位架构师既隶属于开发组,又隶属于架构组,对象之间的关系并不是严格意义上的层次结构,在这种情况下,是不适合使用组合模式,该架构师很可能会收到两份过节费。

这种复合情况下我们必须给父节点和子节点建立双向映射关系,一个简单的方法是给小组和员工对象都增加集合来保存对方的引用。但是这种相互间的引用相当复杂,而且对象之间产生了过多的耦合性,修改或删除一个对象都变得困难,此时我们可以引入中介者模式来管理这些对象

4.    用职责链模式提高组合模式性能

在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现得不够理想。有时候我们确实可以借助一些技巧,在实际操作中避免遍历整棵树,有一种现成的方案就是借助职责链模式。职责链模式一般需要我们手动去设置链条,但在组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递。或反过来从子对象到父对象传递,直到遇到可以处理该请求的对象为止。


10.9 引用父对象

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

现在来改写扫描文件夹的代码,使得在扫描整个文件夹之前,我们可以先移除某一个具体的文件。

首先改写Folder和File类,在这两个类的构造函数中,增加this.parent属性,并且在调用add方法的时候,正确设置文件或者文件夹的父节点:

var Folder= function(name){this.name = name;this.files = [];this.parent = null;
}Folder.prototype.add = function(file){file.parent = this;this.files.push(file);
}
Folder.prototype.scan = function(){console.log(‘开始扫描文件夹:’+this.name;for(vari= 0,file,files = this.files;file = files[i++];){file.scan();}
}


接下来增加Folder.prototype.remove方法,表示移除该文件夹:

Folder.prototype.remove = function(){if(!this.parent){return ;}if(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 Filde(‘Javascript设计模式与开发实践’))
folder.add(folder1);
folder.add(file1);
foder1.remove();
folder.scan();


10.10 何时使用

表示对象的部分—整体层次结构

客户希望统一对待树中的所有对象


推荐阅读
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Python字典推导式及循环列表生成字典方法
    本文介绍了Python中使用字典推导式和循环列表生成字典的方法,包括通过循环列表生成相应的字典,并给出了执行结果。详细讲解了代码实现过程。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 本文介绍了如何找到并终止在8080端口上运行的进程的方法,通过使用终端命令lsof -i :8080可以获取在该端口上运行的所有进程的输出,并使用kill命令终止指定进程的运行。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
author-avatar
你给的未来丶我不要
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有