JS设计模式之组合模式

定义:

组合模式(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();

通过观察这段代码,我们很容易发现,宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树

image

其中,marcoCommand被称为组合对象,openLightCommand 、openComputerCommand 、openQQCommand都是叶对象。在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象。

macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问。

请求在树中传递的过程

在组合模式中,请求在树中传递的过程总是遵循一种逻辑。

以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。

总而言之,如果子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象,请求会继续往下传递。(传递的过程就是重复调用自身对应的方法)叶对象下面不会再有其他子节点,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还会有子节点

image.png
注意:

组合对象可以拥有子节点,叶对象下面就没有子节点,所以我们也许会发生一些误操作,比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加add方法,并且在调用这个方法时,抛出一个异常来及时提醒客户。
具体可参考这篇文章

大家可以动手试试实现这样一个树形结构

├── 菜单
|      ├── 主食类
| |            ├── 米饭
| |            ├── 面
|      ├── 菜类
| |            ├── 酸辣土豆丝
| |            ├── 西红柿炒鸡蛋
|      ├── 汤类
| |            ├── 紫菜蛋花汤
| |            ├── 猪蹄莲藕汤

实战中的组合模式

类似于组合模式的结构其实我们经常碰到,比如浏览器的 DOM 树,从 根节点到 、、