简介
我们平时开发过程中,一定会遇到这种情况:同时处理简单对象和由简单对象组成的复杂对象,这些简单对象和复杂对象会组合成树形结构,在客户端对其处理的时候要保持一致性。比如电商网站中的产品订单,每一张产品订单可能有多个子订单组合,比如操作系统的文件夹,每个文件夹有多个子文件夹或文件,我们作为用户对其进行复制,删除等操作时,不管是文件夹还是文件,对我们操作者来说是一样的。在这种场景下,就非常适合使用组合模式来实现
组合模式
定义
是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的
基本知识
组合模式也是结构型设计模式的一种,它主要体现了整体与部分的诶关系,其典型的应用就是树形结构。组合是一组对象,其中的对象可能包含一个其他对象,也可能包含一组其他对象
组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性
在使用组合模式的使用要注意以下两点
-
组合中既要能包含个体,也要能包含其他组合
-
要抽象出对象和组合的公共特性
组合模式主要有三个角色
-
抽象组件(Component):抽象类,主要定义了参与组合的对象的公共接口
-
子对象(Leaf):组成组合对象的最基本对象
-
组合对象(Composite):由子对象组合起来的复杂对象
理解组合模式的关键是要理解组合模式对单个对象和组合对象使用的一致性,如下解析组合模式的实现加深理解
核心
可以用树形结构来表示这种“部分- 整体”的层次结构
调用组合对象的execute方法,程序会递归调用组合对象 下面的叶对象的execute方法,在后续的实现中体现
但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象,基于这种委托,就需要保证组合对象和叶对象拥有相同的接口
此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作
实现
宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有 一个execute方法负责执行命令
现在我们来造一个“万能遥控器”
// 新建一个关门的命令 const closeDoorCommand = { execute: function(){ console.log( '关门' ) } } // 新建一个开电脑的命令 const openPcCommand = { execute: function(){ console.log( '开电脑' ) } }; // 登陆QQ的命令 const openQQCommand = { execute: function(){ console.log( '登录QQ' ) } }; // 创建一个宏命令 const MacroCommand = function(){ return { // 宏命令的子命令列表 commandsList: [], // 添加命令到子命令列表 add: function( command ){ this.commandsList.push( command ) }, // 依次执行子命令列表里面的命令 execute: function(){ for ( var i = 0, command; command = this.commandsList[ i++ ]; ){ command.execute() } } } } const macroCommand = MacroCommand() macroCommand.add( closeDoorCommand ) macroCommand.add( openPcCommand ) macroCommand.add( openQQCommand ) macroCommand.execute()
其中,marcoCommand被称为组合对象,closeDoorCommand、openPcCommand、openQQCommand都是叶对象。在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象
macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问
组合模式的用途
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性
-
表示树形结构。组合模式有一个优点:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法,所以我们的万能遥控器只需要一次操作,便能依次完成关 门、打开电脑、登录QQ这几件事情。组合模式可以非常方便地描述对象部分-整体层次结构
-
利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象
这在实际开发中会给客户带来相当大的便利性,当我们往万能遥控器里面添加一个命令的时候,并不关心这个命令是宏命令还是普通子命令。这点对于我们不重要,我们只需要确定它是一个命令,并且这个命令拥有可执行的execute方法,那么这个命令就可以被添加进万能遥控器
当宏命令和普通子命令接收到执行execute方法的请求时,宏命令和普通子命令都会做它们各自认为正确的事情。这些差异是隐藏在客户背后的,在客户看来,这种透明性可以让我们非常自由地扩展这个万能遥控器
更强大的宏命令
目前的“万能遥控器”,包含了关门、开电脑、登录QQ这3个命令。现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能
-
打开空调
-
打开电视和音响
-
关门、开电脑、登录QQ
// 创建一个宏命令 const MacroCommand = function(){ return { // 宏命令的子命令列表 commandsList: [], // 添加命令到子命令列表 add: function( command ){ this.commandsList.push( command ) }, // 依次执行子命令列表里面的命令 execute: function(){ for ( var i = 0, command; command = this.commandsList[ i++ ]; ){ command.execute() } } } } <!--打开空调命令--> const openAcCommand = { execute: function(){ console.log( '打开空调' ) } } <!--打开电视和音响--> const openTvCommand = { execute: function(){ console.log( '打开电视' ) } } var openSoundCommand = { execute: function(){ console.log( '打开音响' ) } } // 创建一个宏命令 const macroCommand1 = MacroCommand() // 把打开电视装进这个宏命令里 macroCommand1.add(openTvCommand) // 把打开音响装进这个宏命令里 macroCommand1.add(openSoundCommand) <!--关门、打开电脑和打登录QQ的命令--> const closeDoorCommand = { execute: function(){ console.log( '关门' ) } } const openPcCommand = { execute: function(){ console.log( '开电脑' ) } } const openQQCommand = { execute: function(){ console.log( '登录QQ' ) } }; //创建一个宏命令 const macroCommand2 = MacroCommand() //把关门命令装进这个宏命令里 macroCommand2.add( closeDoorCommand ) //把开电脑命令装进这个宏命令里 macroCommand2.add( openPcCommand ) //把登录QQ命令装进这个宏命令里 macroCommand2.add( openQQCommand ) <!--把各宏命令装进一个超级命令中去--> const macroCommand = MacroCommand() macroCommand.add( openAcCommand ) macroCommand.add( macroCommand1 ) macroCommand.add( macroCommand2 )
透明性带来的安全问题
组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在本质上有是区别的
组合对象可以拥有子节点,叶对象下面就没有子节点,所以我们也许会发生一些误操作,比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加add方法,并且在调用这个方法时,抛出一个异常来及时提醒客户
后续
优缺点
-
优点:可以方便地构造一棵树来表示对象的部分-整体 结构。在树的构造最终 完成之后,只需要通过请求树的最顶层对 象,便能对整棵树做统一一致的操作
-
缺点:创建出来的对象长得都差不多,可能会使代码不好理解,创建太多的对象对性能也会有一些影响
一些值得注意的地方
-
组合模式不是父子关系
组合模式的树型结构容易让人误以为组合对象和叶对象是父子关系,这是不正确的
-
对叶对象操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性。
比如公司要给全体员工发放元旦的过节费1000块,这个场景可以运用组合模式,但如果公司给今天过生日的员工发送一封生日祝福的邮件,组合模式在这里就没有用武之地了,除非先把今天过生日的员工挑选出来。只有用一致的方式对待列表中的每个叶对象的时候,才适合使用组合模式
-
双向映射关系
发放过节费的通知步骤是从公司到各个部门,再到各个小组,最后到每个员工的邮箱里。这本身是一个组合模式的好例子,但要考虑的一种情况是,也许某些员工属于多个组织架构。比如某位架构师既隶属于开发组,又隶属于架构组,对象之间的关系并不是严格意义上的层次结构,在这种情况下,是不适合使用组合模式的,该架构师很可能会收到两份过节费
-
用职责链模式提高组合模式性能
在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现得不够理想。有时候我们确实可以借助一些技巧,在实际操作中避免遍历整棵树,有一种现成的方案是借助职责链模式。职责链模式一般需要我们手动去设置链条,但在组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运用场景之一
何时使用组合模式
组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况
-
表示对象的部分整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放封闭原则
-
客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if 、 else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力
总结
组合模式并不难理解,它主要解决的是单一对象和组合对象在使用方式上的一致性问题。如果对象具有明显的层次结构并且想要统一地使用它们,这就非常适合使用组合模式。在Web开发中,这种层次结构非常常见,很适合使用组合模式,尤其是对于JS来说,不用拘泥于传统面向对象语言的形式,灵活地利用JS语言的特性,达到对部分和整体使用的一致性