8. 组合模式(Composite)

问题:想统一对待单个对象和组合对象(如文件与文件夹)。

核心:定义统一接口,叶子和容器都实现它,容器递归调用子元素。

interface TreeNode {
  display(indent?: string): void;
}

class FileNode implements TreeNode {
  constructor(private name: string) {}
  display(indent = ''): void { console.log(`${indent}- ${this.name}`); }
}

class FolderNode implements TreeNode {
  private children: TreeNode[] = [];

  constructor(private name: string) {}
  add(child: TreeNode): this { this.children.push(child); return this; }
  display(indent = ''): void {
    console.log(`${indent}📁 ${this.name}`);
    this.children.forEach(c => c.display(indent + '  '));
  }
}

const root = new FolderNode('项目')
  .add(new FileNode('index.ts'))
  .add(new FolderNode('src').add(new FileNode('app.ts')).add(new FileNode('utils.ts')));

root.display();
// 📁 项目
//   - index.ts
//   📁 src
//     - app.ts
//     - utils.ts
package composite

import "fmt"

type Component interface {
  Display(indent string)
}

type File struct{ Name string }

func (f File) Display(indent string) {
  fmt.Printf("%s- %s\n", indent, f.Name)
}

type Folder struct {
  Name     string
  Children []Component
}

func (f *Folder) Add(c Component) *Folder {
  f.Children = append(f.Children, c)
  return f
}

func (f *Folder) Display(indent string) {
  fmt.Printf("%s📁 %s\n", indent, f.Name)
  for _, child := range f.Children {
    child.Display(indent + "  ")
  }
}

实战场景

  • 文件系统:文件和文件夹都能展示、删除、计算大小,文件夹只是递归处理子节点。
  • 前端组件树:页面、布局、组件、文本节点都可以抽象成节点,渲染时递归遍历整棵树。
  • 菜单和导航:菜单项可以是叶子,也可以包含子菜单,权限过滤、排序和渲染都可以统一处理。
  • 组织架构:员工、团队、部门、公司形成树状结构,统计人数、预算、权限时递归汇总。
  • 评论系统:评论和回复都可以作为节点,渲染、折叠、删除、计数使用统一接口。
  • 权限模型:角色、权限组、单个权限组成树,判断权限时逐层聚合。
  • 表达式树:规则引擎里单个条件和 AND/OR 条件组都实现同一个 evaluate()
  • 图层系统:设计工具里图形、图片、文本、分组图层都能移动、隐藏、复制和导出。
  • 任务拆解:项目、里程碑、任务、子任务使用统一接口,进度和工时从叶子向上汇总。
  • AST:编译器或 Markdown 解析器把文档表示成树,文本、标题、列表、代码块都作为节点处理。