组合模式

用树形结构表示部分-整体关系,统一处理单个对象和组合对象

问题

什么是组合模式?如何在前端中实现和应用?

解答

组合模式将对象组合成树形结构,让客户端可以用统一的方式处理单个对象和组合对象。

基础实现:文件系统

// 抽象组件 - 定义统一接口
class FileSystemNode {
  constructor(name) {
    this.name = name;
  }

  getSize() {
    throw new Error('子类必须实现 getSize 方法');
  }

  print(indent = '') {
    throw new Error('子类必须实现 print 方法');
  }
}

// 叶子节点 - 文件
class File extends FileSystemNode {
  constructor(name, size) {
    super(name);
    this.size = size;
  }

  getSize() {
    return this.size;
  }

  print(indent = '') {
    console.log(`${indent}📄 ${this.name} (${this.size}KB)`);
  }
}

// 组合节点 - 文件夹
class Folder extends FileSystemNode {
  constructor(name) {
    super(name);
    this.children = [];
  }

  add(node) {
    this.children.push(node);
    return this;
  }

  remove(node) {
    const index = this.children.indexOf(node);
    if (index > -1) {
      this.children.splice(index, 1);
    }
    return this;
  }

  // 递归计算所有子节点大小
  getSize() {
    return this.children.reduce((total, child) => total + child.getSize(), 0);
  }

  print(indent = '') {
    console.log(`${indent}📁 ${this.name} (${this.getSize()}KB)`);
    this.children.forEach(child => child.print(indent + '  '));
  }
}

// 使用示例
const root = new Folder('项目');
const src = new Folder('src');
const components = new Folder('components');

components.add(new File('Button.jsx', 5));
components.add(new File('Modal.jsx', 8));

src.add(components);
src.add(new File('index.js', 2));
src.add(new File('App.jsx', 10));

root.add(src);
root.add(new File('package.json', 1));
root.add(new File('README.md', 3));

root.print();
// 📁 项目 (29KB)
//   📁 src (25KB)
//     📁 components (13KB)
//       📄 Button.jsx (5KB)
//       📄 Modal.jsx (8KB)
//     📄 index.js (2KB)
//     📄 App.jsx (10KB)
//   📄 package.json (1KB)
//   📄 README.md (3KB

console.log('总大小:', root.getSize() + 'KB'); // 29KB

实际应用:菜单组件

// 菜单项(叶子)
class MenuItem {
  constructor(name, action) {
    this.name = name;
    this.action = action;
  }

  render() {
    const li = document.createElement('li');
    li.textContent = this.name;
    li.onclick = this.action;
    li.className = 'menu-item';
    return li;
  }
}

// 子菜单(组合)
class SubMenu {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(item) {
    this.children.push(item);
    return this;
  }

  render() {
    const li = document.createElement('li');
    li.className = 'submenu';

    const span = document.createElement('span');
    span.textContent = this.name + ' ▸';
    li.appendChild(span);

    const ul = document.createElement('ul');
    this.children.forEach(child => ul.appendChild(child.render()));
    li.appendChild(ul);

    return li;
  }
}

// 构建菜单
const fileMenu = new SubMenu('文件')
  .add(new MenuItem('新建', () => console.log('新建文件')))
  .add(new MenuItem('打开', () => console.log('打开文件')))
  .add(new SubMenu('最近打开')
    .add(new MenuItem('project1.js', () => {}))
    .add(new MenuItem('project2.js', () => {}))
  );

const editMenu = new SubMenu('编辑')
  .add(new MenuItem('撤销', () => console.log('撤销')))
  .add(new MenuItem('重做', () => console.log('重做')));

// 渲染到页面
const nav = document.createElement('ul');
nav.className = 'menu';
nav.appendChild(fileMenu.render());
nav.appendChild(editMenu.render());

简化版:函数式实现

// 用对象字面量表示树结构
const menuConfig = {
  type: 'menu',
  children: [
    {
      type: 'submenu',
      name: '文件',
      children: [
        { type: 'item', name: '新建', action: 'create' },
        { type: 'item', name: '保存', action: 'save' },
      ]
    },
    { type: 'item', name: '帮助', action: 'help' }
  ]
};

// 递归渲染
function renderMenu(node) {
  if (node.type === 'item') {
    return `<li class="item">${node.name}</li>`;
  }

  if (node.type === 'submenu') {
    const children = node.children.map(renderMenu).join('');
    return `<li class="submenu"><span>${node.name}</span><ul>${children}</ul></li>`;
  }

  if (node.type === 'menu') {
    const children = node.children.map(renderMenu).join('');
    return `<ul class="menu">${children}</ul>`;
  }
}

console.log(renderMenu(menuConfig));

关键点

  • 统一接口:叶子节点和组合节点实现相同的接口,客户端无需区分
  • 递归结构:组合节点包含子节点,形成树形结构
  • 透明性:对单个对象和组合对象的操作一致
  • 典型场景:文件系统、DOM 树、菜单导航、组织架构
  • React 中的体现:组件嵌套、children prop 就是组合模式的应用