Skip to content

4.4 组合模式 (Composite Pattern)

欢迎来到结构型模式的最后一站!我们将学习一个旨在处理“部分-整体”层次结构的模式——组合模式。它能让你像对待单个对象一样,统一地处理对象组合。

核心定义:将对象组合成树形结构,以表示“部分-整体”的层次关系。组合模式使得客户端对单个对象(叶子)和组合对象(容器)的使用具有一致性。

一、生活中的场景:文件与文件夹

这个模式最完美的现实世界映射就是电脑的文件系统。

  • 文件 (Leaf / 叶子节点):如 photo.jpg, document.txt。它们是基本单位,内部不能再包含其他东西。
  • 文件夹 (Composite / 组合节点):如 My Documents, Downloads。它们是容器,既可以包含文件,也可以包含文件夹,形成无限嵌套的树形结构。

现在,思考一个操作:计算大小

  • 对于一个文件,计算大小很简单,就是它自身的体积。
  • 对于一个文件夹,计算它的大小,你需要遍历它内部所有的文件和子文件夹,把它们的大小全部加起来。

组合模式的精髓就在于,它能让你用完全相同的方式来对待“文件”和“文件夹”。你可以对任何一个节点(不管是文件还是文件夹)调用 calculateSize() 方法,而无需关心它的具体类型。

二、没有组合模式的代码:繁琐的递归与判断

让我们用代码模拟一个需要区别对待文件和文件夹的场景。

javascript
// 两个完全独立的类
class File {
  constructor(name, size) {
    this.name = name;
    this.size = size;
  }
}

class Folder {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }
}

// --- 客户端代码 ---
// 我们需要一个函数来计算一个文件夹的总大小
function calculateTotalSize(node) {
  let total = 0;

  // 必须进行类型判断!
  if (node instanceof Folder) {
    for (const child of node.children) {
      // 对每个子节点,都需要递归调用,并再次进行类型判断
      total += calculateTotalSize(child);
    }
  } else if (node instanceof File) {
    total += node.size;
  }

  return total;
}

// 创建一个文件结构
const root = new Folder("Root");
const musicFolder = new Folder("Music");
const videoFolder = new Folder("Videos");
const song1 = new File("song1.mp3", 10);
const song2 = new File("song2.mp3", 15);
const movie1 = new File("movie1.mp4", 100);

root.add(musicFolder);
root.add(videoFolder);
musicFolder.add(song1);
musicFolder.add(song2);
videoFolder.add(movie1);

// 使用起来非常不便
console.log(`Music 文件夹大小: ${calculateTotalSize(musicFolder)}`); // 25
console.log(`Root 文件夹大小: ${calculateTotalSize(root)}`); // 125

这段代码的问题在于,客户端的 calculateTotalSize 函数充满了 instanceof 判断,它被迫要知道 FileFolder 这两个具体类的实现差异。这使得代码非常不“透明”,且难以扩展。

三、使用组合模式重构

核心思想是:为叶子节点 (File) 和组合节点 (Folder) 定义一个共同的接口。

第 1 步:定义组件 (Component) 接口

虽然 JavaScript 没有正式的接口,但我们可以约定,所有节点都必须有 getSize() 方法。

第 2 步:实现叶子 (Leaf)

javascript
class File {
  constructor(name, size) {
    this.name = name;
    this.size = size;
  }

  // 实现共同接口
  getSize() {
    return this.size;
  }

  // 为了保持接口统一(透明性),叶子节点也需要提供 add/remove 方法,
  // 但通常是空实现或抛出错误,因为文件不能有子节点。
  add() {
    throw new Error("文件不支持添加子节点");
  }
}

第 3 步:实现组合 (Composite)

javascript
class Folder {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }

  // 实现共同接口
  getSize() {
    let total = 0;
    // **关键**:它不自己计算,而是“委托”给它的每一个子节点
    for (const child of this.children) {
      total += child.getSize(); // 无论 child 是文件还是文件夹,都有 getSize 方法!
    }
    return total;
  }
}

第 4 步:重构客户端代码

现在,客户端代码变得极其简单和“透明”。

javascript
// 创建文件结构的过程和之前一样...
const root = new Folder("Root");
// ...

// 客户端现在可以统一对待所有节点了!
console.log(`Music 文件夹大小: ${musicFolder.getSize()}`); // 25
console.log(`Root 文件夹大小: ${root.getSize()}`); // 125
console.log(`movie1 文件大小: ${movie1.getSize()}`); // 100

客户端不再需要 calculateTotalSize 这个外部函数,也不再需要任何 instanceof 判断。它只需要知道,任何一个节点,无论是文件还是文件夹,都可以调用 getSize()

四、核心概念:透明性 vs. 安全性

组合模式在实现时有一个经典的设计权衡:

  • 透明组合模式 (我们上面实现的):将 add, remove 等管理子节点的方法也定义在共同接口中。
    • 优点:对客户端是完全透明的,客户端可以无差别地对待所有组件。
    • 缺点:对叶子节点“不安全”,因为它被迫实现了它本不该有的方法(如文件的 add)。
  • 安全组合模式:只在组合节点 (Folder) 中定义 add, remove 等方法,共同接口中只有 getSize 这种共有的方法。
    • 优点:对叶子节点是“安全”的,它不会有多余的方法。
    • 缺点:破坏了透明性,客户端在需要添加子节点时,必须先判断 instanceof Folder

在绝大多数情况下,透明组合模式更受欢迎,因为它带来的客户端代码的简化和一致性,其价值远大于对叶子节点造成的一点点“不安全”。

五、优缺点与适用场景

优点

  1. 统一接口,简化客户端:客户端代码可以统一处理所有对象,无需区分它是叶子还是组合。
  2. 易于扩展:可以很方便地增加新的叶子或组合类,只要它们实现了共同接口,原有代码无需改动。
  3. 天然支持递归:非常适合用来表示和处理具有递归结构的树形数据。

缺点

  1. 设计更抽象:可能会让设计变得过于通用,使得限制组合中组件的类型变得困难。
  2. 透明模式下的“不安全”:如上所述,叶子节点可能会继承到它不支持的方法。

适用场景

  • 任何你需要表示“部分-整体”的树形结构时。
  • UI 界面:一个窗口 (Composite) 包含面板 (Composite),面板又包含按钮、文本框 (Leafs)。
  • 组织架构:一个部门 (Composite) 包含子部门 (Composite) 和员工 (Leaf)。
  • 代码解析:抽象语法树 (AST) 就是一个典型的组合模式应用。

结构型模式小结

恭喜你!我们已经探索完了所有核心的结构型模式。它们的核心都是关于如何组织关联对象,以创造出更灵活、更强大的系统结构。

  • 适配器模式:解决接口不兼容的问题。
  • 代理模式:解决访问控制增强职责的问题。
  • 外观模式:解决简化复杂子系统接口的问题。
  • 装饰器模式:解决动态添加功能的问题。
  • 组合模式:解决统一处理树形结构中部分与整体的问题。