定义:
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
特点:
- 用小的子对象构造更大的父对象,而这些子对象也由更小的子对象构成
- 单个对象和组合对象对于用户暴露的接口具有一致性,而同种接口不同表现形式亦体现了多态性
应用场景
组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。
如何解决:树枝和叶子实现统一接口,树枝内部组合该接口。
关键代码:树枝内部组合该接口,并且含有内部属性 List,里面放 Component。
下面来看一个具体案例
首先有一个Folder类,里面可以添加file,扫描文件夹,然后创建一个File类,扫描文件
// 创建文件类
class File {
constructor(name) {
this.name = name;
}
scan() {
console.log('正在扫描文件 ' + this.name);
}
}
// 创建文件夹类
class Folder {
constructor(name) {
this.name = name;
this.files = [];
}
// add file
add(file) {
this.files.push(file);
}
// scan
scan() {
console.log('正在扫描文件夹 ' + this.name);
for (let item of this.files) {
item.scan();
}
}
}
// 创建根目录
const home = new Folder('home');
// 创建目录1
const folder1 = new Folder('folder1');
// 创建文件1
const file1 = new File('file1');
// 将文件添加到目录1
folder1.add(file1);
// 将目录1添加到根目录
home.add(folder1);
// 创建目录2
const folder2 = new Folder('folder2');
// 创建文件2
const file2 = new File('file2');
// 将文件2添加到目录2
folder2.add(file2);
// 创建文件3
const file3 = new File('file3');
// 将文件3添加到目录2
folder2.add(file3);
// 将目录2添加到根目录
home.add(folder2);
// 开始从根目录扫描
home.scan();
/*
正在扫描文件夹 home
正在扫描文件夹 folder1
正在扫描文件 file1
正在扫描文件夹 folder2
正在扫描文件 file2
正在扫描文件 file3
*/
这里用代码模拟文件扫描功能,封装了File和Folder两个类。在组合模式下,用户可以向Folder类嵌套File或者Folder来模拟真实的“文件目录”的树结构。
同时,两个类都对外提供了scan接口,File下的scan是扫描文件,Folder下的scan是调用子文件夹和子文件的scan方法。整个过程采用的是深度优先。
class MacroCommand {
constructor() {
this.commandList = [];
}
add(...command) {
if (Array.isArray(command)) {
command.forEach(c => {
this.commandList.push(c);
})
} else {
this.commandList.push(command);
}
}
execute() {
this.commandList.forEach(command => {
command.execute(command);
})
}
}
class Command {
constructor(name) {
this.name = name;
}
execute(command) {
console.log('执行 ' + command.name + '命令')
}
}
// 新建单个命令
const macroCommand1 = new MacroCommand();
const openLightCommand = new Command('打开灯');
macroCommand1.add(openLightCommand);
// 新建组合命令
const macroCommand2 = new MacroCommand();
const openComputerCommand = new Command('组合-打开电脑');
const openQQCommand = new Command('组合-登录QQ');
macroCommand2.add(openComputerCommand);
macroCommand2.add(openQQCommand );
const macroCommand = new MacroCommand();
// 将组合命令添加到主命令中
// macroCommand.add(macroCommand1);
// macroCommand.add(macroCommand2);
macroCommand.add(macroCommand1, macroCommand2)
// 执行主命令
macroCommand.execute();
通过观察这段代码,我们很容易发现,宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树
其中,marcoCommand被称为组合对象,openLightCommand 、openComputerCommand 、openQQCommand都是叶对象。在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象。
macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问。
请求在树中传递的过程
在组合模式中,请求在树中传递的过程总是遵循一种逻辑。
以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。
总而言之,如果子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象,请求会继续往下传递。(传递的过程就是重复调用自身对应的方法)叶对象下面不会再有其他子节点,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还会有子节点
注意:
组合对象可以拥有子节点,叶对象下面就没有子节点,所以我们也许会发生一些误操作,比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加add方法,并且在调用这个方法时,抛出一个异常来及时提醒客户。
具体可参考这篇文章
大家可以动手试试实现这样一个树形结构
├── 菜单
| ├── 主食类
| | ├── 米饭
| | ├── 面
| ├── 菜类
| | ├── 酸辣土豆丝
| | ├── 西红柿炒鸡蛋
| ├── 汤类
| | ├── 紫菜蛋花汤
| | ├── 猪蹄莲藕汤
实战中的组合模式
类似于组合模式的结构其实我们经常碰到,比如浏览器的 DOM 树,从
根节点到