组合模式的定义
组合模式:又叫 “部分整体” 模式,将对象组合成树形结构,以表示 “部分-整体” 的层次结构。通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
特点:
1. 透明性——树叶对象接口保持统一,外部调用时无需区分。(需注意的问题,如文件目录的例子,文件(叶对象)下不可再添加文件,因此需在文件类的 add()
方法中抛出异常,以作提醒。)
2. 自上而下的的请求流向,从树对象传递给叶对象;
3. 调用顶层对象,会自行遍历其下的叶对象执行。
使用场景
组合模式如果运用得当,可以大大简化代码
- 优化处理递归或分级数据结构(文件系统 - 目录文件管理);
- 与其它设计模式联用,如与命令模式联用实现 “宏命令”。
- 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是我们在开发期间不确定这棵 树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式 中增加和删除树的节点非常方便,并且符合开放-封闭原则。
- 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆if、else语句来分别处理它们。组合对象和叶对象会各自做自己正确的事 情,这是组合模式最重要的能力。
优点
- 忽略组合对象和单个对象的差别,对外一致接口使用;
- 解耦调用者与复杂元素之间的联系,处理方式变得简单。
缺点
- 树叶对象接口一致,无法区分,只有在运行时方可辨别;
- 包裹对象创建太多,额外增加内存负担。
注意事项:
1. 组合不是继承,树叶对象并不是父子对象
组合模式的树型结构是一种 HAS-A(聚合)的关系,而不是 IS-A 。树叶对象能够合作的关键,是它们对外保持统一接口,而不是叶对象继承树对象的属性方法,两者之间不是父子关系。
2. 叶对象操作保持一致性
叶对象除了与树对象接口一致外,操作也必须保持一致性。一片叶子只能生在一颗树上。调用顶层对象时,每个叶对象只能接收一次请求,一个叶对象不能从属多个树对象。
3. 叶对象实现冒泡传递
请求传递由树向叶传递,如果想逆转传递过程,需在叶对象中保留对树对象的引用,冒泡传递给树对象处理。
4. 不只是简单的子集遍历
调用对象的接口方法时,如果该对象是树对象,则会将请求传递给叶对象,由叶对象执行方法,以此类推。不同于迭代器模式,迭代器模式遍历并不会做请求传导。
5. 用职责链模式提高组合模式性能
在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现得不够理想。有时候我们确实可以借助一些技巧,在实际操作中避免遍历整棵树,有一种现成的方案是借助职责链模式。职责链模式一般需要我们手动去设置链条,但在组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运用场景之一。
演示范例1 ——组合模式实现文件目录
JavaScript实现
// 树对象 - 文件目录 class CFolder { constructor(name) { this.name = name; this.files = []; } add(file) { this.files.push(file); } scan() { for (let file of this.files) { file.scan(); } } } // 叶对象 - 文件 class CFile { constructor(name) { this.name = name; } add(file) { throw new Error('文件下面不能再添加文件'); } scan() { console.log(`开始扫描文件:${this.name}`); } } let mediaFolder = new CFolder('娱乐'); let movieFolder = new CFolder('电影'); let musicFolder = new CFolder('音乐'); let file1 = new CFile('钢铁侠.mp4'); let file2 = new CFile('再谈记忆.mp3'); movieFolder.add(file1); musicFolder.add(file2); mediaFolder.add(movieFolder); mediaFolder.add(musicFolder); mediaFolder.scan(); /* 输出: 开始扫描文件:钢铁侠.mp4 开始扫描文件:再谈记忆.mp3 */
CFolder
与 CFile
接口保持一致。执行 scan()
时,若发现是树对象,则继续遍历其下的叶对象,执行 scan()
。
TypeScript实现
JavaScript 实现组合模式的难点是保持树对象与叶对象之间接口保持统一,可借助 TypeScript 定制接口规范,实现类型约束。
// 定义接口规范 interface Compose { name: string, add(file: CFile): void, scan(): void } // 树对象 - 文件目录 class CFolder implements Compose { fileList = []; name: string; constructor(name: string) { this.name = name; } add(file: CFile) { this.fileList.push(file); } scan() { for (let file of this.fileList) { file.scan(); } } } // 叶对象 - 文件 class CFile implements Compose { name: string; constructor(name: string) { this.name = name; } add(file: CFile) { throw new Error('文件下面不能再添加文件'); } scan() { console.log(`开始扫描:${this.name}`) } } let mediaFolder = new CFolder('娱乐'); let movieFolder = new CFolder('电影'); let musicFolder = new CFolder('音乐'); let file1 = new CFile('钢铁侠.mp4'); let file2 = new CFile('再谈记忆.mp3'); movieFolder.add(file1); musicFolder.add(file2); mediaFolder.add(movieFolder); mediaFolder.add(musicFolder); mediaFolder.scan(); /* 输出: 开始扫描文件:钢铁侠.mp4 开始扫描文件:再谈记忆.mp3 */
演示范例2 ——组合模式实现万能遥控器
现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能:
- 打开空调
- 打开电视和音响
- 关门、开电脑、登录QQ
// 创建一个宏命令 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 openAcCommand = { execute: function(){ console.log( '打开空调' ); } }; <!--打开电视和音响--> var openTvCommand = { execute: function(){ console.log( '打开电视' ); } }; var openSoundCommand = { execute: function(){ console.log( '打开音响' ); } }; //创建一个宏命令 var macroCommand1 = MacroCommand(); //把打开电视装进这个宏命令里 macroCommand1.add(openTvCommand) //把打开音响装进这个宏命令里 macroCommand1.add(openSoundCommand) <!--关门、打开电脑和打登录QQ的命令--> var closeDoorCommand = { execute: function(){ console.log( '关门' ); } }; var openPcCommand = { execute: function(){ console.log( '开电脑' ); } }; var openQQCommand = { execute: function(){ console.log( '登录QQ' ); } }; //创建一个宏命令 var macroCommand2 = MacroCommand(); //把关门命令装进这个宏命令里 macroCommand2.add( closeDoorCommand ); //把开电脑命令装进这个宏命令里 macroCommand2.add( openPcCommand ); //把登录QQ命令装进这个宏命令里 macroCommand2.add( openQQCommand ); <!--把各宏命令装进一个超级命令中去--> var macroCommand = MacroCommand(); macroCommand.add( openAcCommand ); macroCommand.add( macroCommand1 ); macroCommand.add( macroCommand2 );
基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。在树最终被构造完成之后,让整颗树最终运转起来的步骤非常简单,只需要调用最上层对象的execute方法。每当对最上层的对象进行一次请求时,实际上是在对整个树进行深度优先的搜索,而创建组合对象的程序员并不关心这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情。
更多设计模式详见——js设计模式【详解】总目录
https://blog.csdn.net/weixin_41192489/article/details/116154815