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

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

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


推荐阅读
  • 本文探讨了 Kafka 集群的高效部署与优化策略。首先介绍了 Kafka 的下载与安装步骤,包括从官方网站获取最新版本的压缩包并进行解压。随后详细讨论了集群配置的最佳实践,涵盖节点选择、网络优化和性能调优等方面,旨在提升系统的稳定性和处理能力。此外,还提供了常见的故障排查方法和监控方案,帮助运维人员更好地管理和维护 Kafka 集群。 ... [详细]
  • 本文探讨了资源访问的学习路径与方法,旨在帮助学习者更高效地获取和利用各类资源。通过分析不同资源的特点和应用场景,提出了多种实用的学习策略和技术手段,为学习者提供了系统的指导和建议。 ... [详细]
  • 本文详细探讨了Zebra路由软件中的线程机制及其实际应用。通过对Zebra线程模型的深入分析,揭示了其在高效处理网络路由任务中的关键作用。文章还介绍了线程同步与通信机制,以及如何通过优化线程管理提升系统性能。此外,结合具体应用场景,展示了Zebra线程机制在复杂网络环境下的优势和灵活性。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 具备括号和分数功能的高级四则运算计算器
    本研究基于C语言开发了一款支持括号和分数运算的高级四则运算计算器。该计算器通过模拟手算过程,对每个运算符进行优先级标记,并按优先级从高到低依次执行计算。其中,加减运算的优先级最低,为0。此外,该计算器还支持复杂的分数运算,能够处理包含括号的表达式,提高了计算的准确性和灵活性。 ... [详细]
  • 本文总结了JavaScript的核心知识点和实用技巧,涵盖了变量声明、DOM操作、事件处理等重要方面。例如,通过`event.srcElement`获取触发事件的元素,并使用`alert`显示其HTML结构;利用`innerText`和`innerHTML`属性分别设置和获取文本内容及HTML内容。此外,还介绍了如何在表单中动态生成和操作``元素,以便更好地处理用户输入。这些技巧对于提升前端开发效率和代码质量具有重要意义。 ... [详细]
  • 设计实战 | 10个Kotlin项目深度解析:首页模块开发详解
    设计实战 | 10个Kotlin项目深度解析:首页模块开发详解 ... [详细]
  • Python 实战:异步爬虫(协程技术)与分布式爬虫(多进程应用)深入解析
    本文将深入探讨 Python 异步爬虫和分布式爬虫的技术细节,重点介绍协程技术和多进程应用在爬虫开发中的实际应用。通过对比多进程和协程的工作原理,帮助读者理解两者在性能和资源利用上的差异,从而在实际项目中做出更合适的选择。文章还将结合具体案例,展示如何高效地实现异步和分布式爬虫,以提升数据抓取的效率和稳定性。 ... [详细]
  • 在探讨C语言编程文本编辑器的最佳选择与专业推荐时,本文将引导读者构建一个基础的文本编辑器程序。该程序不仅能够打开并显示文本文件的内容及其路径,还集成了菜单和工具栏功能,为用户提供更加便捷的操作体验。通过本案例的学习,读者可以深入了解文本编辑器的核心实现机制。 ... [详细]
  • 本次发布的Qt音乐播放器2.0版本在用户界面方面进行了细致优化,提升了整体的视觉效果和用户体验。尽管核心功能与1.0版本保持一致,但界面的改进使得操作更加直观便捷,为用户带来了更为流畅的使用体验。此外,我们还对部分细节进行了微调,以确保软件的稳定性和性能得到进一步提升。 ... [详细]
  • 技术日志:使用 Ruby 爬虫抓取拉勾网职位数据并生成词云分析报告
    技术日志:使用 Ruby 爬虫抓取拉勾网职位数据并生成词云分析报告 ... [详细]
  • 深入解析:React与Webpack配置进阶指南(第二部分)
    在本篇进阶指南的第二部分中,我们将继续探讨 React 与 Webpack 的高级配置技巧。通过实际案例,我们将展示如何使用 React 和 Webpack 构建一个简单的 Todo 应用程序,具体包括 `TodoApp.js` 文件中的代码实现,如导入 React 和自定义组件 `TodoList`。此外,我们还将深入讲解 Webpack 配置文件的优化方法,以提升开发效率和应用性能。 ... [详细]
  • 本文探讨了利用Java实现WebSocket实时消息推送技术的方法。与传统的轮询、长连接或短连接等方案相比,WebSocket提供了一种更为高效和低延迟的双向通信机制。通过建立持久连接,服务器能够主动向客户端推送数据,从而实现真正的实时消息传递。此外,本文还介绍了WebSocket在实际应用中的优势和应用场景,并提供了详细的实现步骤和技术细节。 ... [详细]
  • 本文详细介绍了 Windows API 中的按钮控件及其应用实例。主要功能包括:1. `CheckDlgButton` 用于更改对话框中按钮的选中状态;2. `CheckRadioButton` 用于设置单选按钮的选中状态。此外,还探讨了按钮控件在实际开发中的多种应用场景,帮助开发者更好地理解和使用这些功能。 ... [详细]
  • 本文详细探讨了OpenCV中人脸检测算法的实现原理与代码结构。通过分析核心函数和关键步骤,揭示了OpenCV如何高效地进行人脸检测。文章不仅提供了代码示例,还深入解释了算法背后的数学模型和优化技巧,为开发者提供了全面的理解和实用的参考。 ... [详细]
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社区 版权所有