热门标签 | 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 何时使用

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

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


推荐阅读
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 在编写业务代码时,常常会遇到复杂的业务逻辑导致代码冗长混乱的情况。为了解决这个问题,可以利用中间件模式来简化代码逻辑。中间件模式可以帮助我们更好地设计架构和代码,提高代码质量。本文介绍了中间件模式的基本概念和用法。 ... [详细]
  • 本文介绍了在wepy中运用小顺序页面受权的计划,包含了用户点击作废后的从新受权计划。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • 解决.net项目中未注册“microsoft.ACE.oledb.12.0”提供程序的方法
    在开发.net项目中,通过microsoft.ACE.oledb读取excel文件信息时,报错“未在本地计算机上注册“microsoft.ACE.oledb.12.0”提供程序”。本文提供了解决这个问题的方法,包括错误描述和代码示例。通过注册提供程序和修改连接字符串,可以成功读取excel文件信息。 ... [详细]
  • 如何在HTML中获取鼠标的当前位置
    本文介绍了在HTML中获取鼠标当前位置的三种方法,分别是相对于屏幕的位置、相对于窗口的位置以及考虑了页面滚动因素的位置。通过这些方法可以准确获取鼠标的坐标信息。 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
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社区 版权所有