JavaScript设计模式-结构型-组合模式

2021-07-01 JavaScript设计模式 阅读 685 次

概要

组合模式是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

JavaScript设计模式-结构型-组合模式-黄继鹏博客

问题

例如, 你有两类对象: 产品盒子 。 一个盒子中可以包含多个产品或者几个较小的盒子 。 这些小盒子中同样可以包含一些产品或更小的盒子 , 以此类推。

假设你希望在这些类的基础上开发一个定购系统。 订单中可以包含无包装的简单产品, 也可以包含装满产品的盒子……以及其他盒子。 此时你会如何计算每张订单的总价格呢?

JavaScript设计模式-结构型-组合模式-黄继鹏博客

订单中可能包括各种产品, 这些产品放置在盒子中, 然后又被放入一层又一层更大的盒子中。 整个结构看上去像是一棵倒过来的树。

你可以尝试直接计算: 打开所有盒子, 找到每件产品, 然后计算总价。 这在真实世界中或许可行, 但在程序中, 你并不能简单地使用循环语句来完成该工作。 你必须事先知道所有产品盒子的类别, 所有盒子的嵌套层数以及其他繁杂的细节信息。 因此, 直接计算极不方便, 甚至完全不可行。

解决方案

组合模式建议使用一个通用接口来与产品盒子进行交互, 并且在该接口中声明一个计算总价的方法。

那么方法该如何设计呢? 对于一个产品, 该方法直接返回其价格; 对于一个盒子, 该方法遍历盒子中的所有项目, 询问每个项目的价格, 然后返回该盒子的总价格。 如果其中某个项目是小一号的盒子, 那么当前盒子也会遍历其中的所有项目, 以此类推, 直到计算出所有内部组成部分的价格。 你甚至可以在盒子的最终价格中增加额外费用, 作为该盒子的包装费用。

JavaScript设计模式-结构型-组合模式-黄继鹏博客

组合模式以递归方式处理对象树中的所有项目

该方式的最大优点在于你无需了解构成树状结构的对象的具体类。 你也无需了解对象是简单的产品还是复杂的盒子。 你只需调用通用接口以相同的方式对其进行处理即可。 当你调用该方法后, 对象会将请求沿着树结构传递下去。

组合模式结构图

JavaScript设计模式-结构型-组合模式-黄继鹏博客

  1. 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作。

  2. 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。

    一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。

  3. 容器 (Container)——又名 “组合 (Composite)”——是包含叶节点或其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。

    容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。

  4. 客户端 (Client) 通过组件接口与所有项目交互。 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。

举例

扫描文件夹

文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树。

当我用杀毒软件扫描某个文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹进行扫描,就可以得到“汇总” 结果。

当我们找到某些文件,想要进行批量操作时,并不需要考虑这批文件的类型,不管它们是单独的文件还是被放在了文件夹中。客户端只需要调用组合模式的共有的接口,就可以与非常复杂的对象结构进行交互。

/**
 * 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作
 */
class Component {
  constructor(name) {
    this.name = name
  }

  add() {
    throw new Error('add 未实现')
  }

  remove() {
    throw new Error('remove 未实现')
  }

  scan() {
    throw new Error('scan 未实现')
  }
}

/**
 * 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。
 */
class File extends Component {
  constructor(name) {
    super(name)
    this.parent = null
  }

  add(file) {
    throw new Error('文件下面不能再添加文件')
  }

  scan(placeholder) {
    console.log(placeholder + "└──" + this.name)
  }
}

/**
 * 容器 (Container)——又名 “组合 (Composite)”——是包含叶节点或
 * 其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过
 * 通用的组件接口与其子项目交互。
 */
class Folder extends Component {
  constructor(name) {
    super(name)
    this.parent = null
    this.files = []
  }

  add(file) {
    file.parent = this
    this.files.push(file)
    return this
  }

  remove(file) {
    const fileIndex = this.files.indexOf(file)
    this.files.splice(fileIndex, 1)

    file.parent = null
  }

  scan(placeholder) {
    console.log(placeholder + "└──" + this.name)
    for (const file of this.files) {
      file.scan(placeholder + "		")
    }
  }
}

/**
 * 客户端 (Client) 通过组件接口与所有项目交互。
 * 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互
 */
function clientCode(tree) {
  console.group()
  tree.scan('')
  console.groupEnd()
}


let tree = new Folder('根目录')
let branch1 = new Folder('目录一')
let branch2 = new Folder('目录二')
let branch3 = new Folder('目录三')

let file1 = new File('视频文件1.mp4')
let file2 = new File('视频文件2.mp3')
let file3 = new File('视频文件3.mp3')

tree.add(file1)
tree.add(file2)
tree.add(
  branch1
  .add(new Folder('子文件夹一'))
  .add(new Folder('子文件夹二'))
  .add((new Folder('子文件夹三')).add(new File('视频文件4.mp3')))
);
tree.add(branch2.add(file3))
tree.add(branch3)
tree.remove(branch3)

clientCode(tree)

output

"└──根目录"
"   └──视频文件1.mp4"
"   └──视频文件2.mp3"
"   └──目录一"
"       └──子文件夹一"
"       └──子文件夹二"
"       └──子文件夹三"
"           └──视频文件4.mp3"
"   └──目录二"
"       └──视频文件3.mp3"

总结

优缺点

  • 优点
    • 可以利用多态和递归机制更方便地使用复杂树结构
    • 开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分
  • 缺点
    • 对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。
    • 树叶对象接口一致,无法区分,只有在运行时方可辨别,使代码难以理解。
    • 包裹对象创建太多,额外增加内存负担

适合应用场景

  • 需要实现树状对象结构, 可以使用组合模式

    组合模式为你提供了两种共享公共接口的基本元素类型: 简单叶节点和复杂容器。 容器中可以包含叶节点和其他容器。 这使得你可以构建树状嵌套递归对象结构。

  • 如果希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式

    组合模式使客户可以忽略组合对象和叶对象的区别, 客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就 不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情, 这是组合模式最重要的能力

0条评论
...