组合模式深度解析

  1. 组合模式:
    1. 是一种结构型设计模式,核心思想是将对象组合成树形结构,以表示 “部分 - 整体” 的层次关系,让客户端能以统一的方式处理单个对象 (叶子节点) 和对象组合 (容器节点)
    2. 简单比喻:把 “文件” (叶子) 和 “文件夹” (容器) 统一视为 “文件系统节点”,不管点击文件还是文件夹,都能执行 “查看大小”、“删除” 等操作,无需区分类型;
  2. 类图:
  3. 实现代码:
    // ---------------------- 抽象组件(Component) ----------------------
    abstract class MenuComponent {
      /** 通用操作:渲染组件 */
      abstract render(): void;
    
      /** 添加子组件(叶子节点空实现) */
      add(component: MenuComponent): void {
        throw new Error("当前组件不支持添加子节点");
      }
    
      /** 移除子组件(叶子节点空实现) */
      remove(component: MenuComponent): void {
        throw new Error("当前组件不支持移除子节点");
      }
    
      /** 获取子组件(叶子节点空实现) */
      getChild(index: number): MenuComponent | null {
        throw new Error("当前组件无可用子节点");
      }
    }
    
    // ---------------------- 叶子节点(Leaf) ----------------------
    class MenuItem extends MenuComponent {
      constructor(private name: string, private url: string) {
        super();
      }
    
      // 实现渲染:叶子节点直接渲染自身
      render(): void {
        console.log(`<a href="${this.url}">${this.name}</a>`);
      }
    
      // 叶子节点无需重写 add/remove/getChild(继承父类的报错实现即可)
    }
    
    // ---------------------- 容器节点(Composite) ----------------------
    class Menu extends MenuComponent {
      private children: MenuComponent[] = [];
    
      constructor(private name: string, private level: number) {
        super();
      }
    
      // 实现渲染:容器节点先渲染自身,再递归渲染所有子节点
      render(): void {
        // 根据层级添加缩进,模拟菜单嵌套
        const indent = "  ".repeat(this.level);
        console.log(`${indent}<div class="menu">${this.name}</div>`);
        // 递归渲染子节点
        this.children.forEach(child => child.render());
      }
    
      // 重写添加子组件
      add(component: MenuComponent): void {
        this.children.push(component);
      }
    
      // 重写移除子组件
      remove(component: MenuComponent): void {
        this.children = this.children.filter(item => item !== component);
      }
    
      // 重写获取子组件
      getChild(index: number): MenuComponent | null {
        return this.children[index] || null;
      }
    }
    
    // ---------------------- 测试代码(客户端使用) ----------------------
    // 1. 创建叶子节点(菜单项)
    const homeItem = new MenuItem("首页", "/home");
    const myItem = new MenuItem("我的", "/my");
    const orderItem = new MenuItem("我的订单", "/my/order");
    const settingItem = new MenuItem("设置", "/my/setting");
    
    // 2. 创建容器节点(子菜单)
    const myMenu = new Menu("用户菜单", 1);
    myMenu.add(orderItem);
    myMenu.add(settingItem);
    
    // 3. 创建根容器(导航栏)
    const navMenu = new Menu("导航栏", 0);
    navMenu.add(homeItem);
    navMenu.add(myItem);
    navMenu.add(myMenu); // 容器节点嵌套容器节点
    
    // 4. 统一渲染(客户端无需区分叶子/容器,直接调用 render)
    console.log("渲染导航菜单:");
    navMenu.render();
    

常用使用场景

表单组件嵌套(表单校验 / 值收集)

  1. 场景说明:表单通常包含 “表单容器 → 分组容器 → 输入框 / 按钮” 的嵌套结构,需要统一收集所有字段值、统一校验所有字段,无需区分 “容器”“表单控件”
  2. 实现代码:
    // 1. 抽象组件
    abstract class FormComponent {
      protected label: string;
      constructor(label: string) {
        this.label = label;
      }
    
      abstract getValue(): any; // 统一取值
      abstract validate(): boolean; // 统一校验
    }
    
    // 2. 叶子节点:输入框
    class Input extends FormComponent {
      private value: string = "";
      constructor(label: string, private required: boolean = false) {
        super(label);
      }
    
      setValue(value: string): void {
        this.value = value;
      }
    
      getValue(): any {
        return { [this.label]: this.value };
      }
    
      validate(): boolean {
        if (this.required && !this.value) {
          console.log(`❌ 「${this.label}」为必填项`);
          return false;
        }
        console.log(`✅ 「${this.label}」校验通过`);
        return true;
      }
    }
    
    // 3. 容器节点:表单分组/表单
    class FormGroup extends FormComponent {
      private children: FormComponent[] = [];
      constructor(label: string) {
        super(label);
      }
    
      add(component: FormComponent): void {
        this.children.push(component);
      }
    
      getValue(): any {
        // 递归收集所有子控件值
        return this.children.reduce((res, child) => ({
          ...res,
          ...child.getValue()
        }), { [this.label]: "分组容器" });
      }
    
      validate(): boolean {
        // 递归校验所有子控件
        console.log(`🔍 开始校验「${this.label}」分组`);
        return this.children.every(child => child.validate());
      }
    }
    
    // 4. 客户端使用
    const usernameInput = new Input("用户名", true);
    usernameInput.setValue("zhangsan");
    const passwordInput = new Input("密码", true);
    passwordInput.setValue(""); // 故意留空,模拟校验失败
    const submitButton = new Input("提交按钮", false);
    
    const loginGroup = new FormGroup("登录表单");
    loginGroup.add(usernameInput);
    loginGroup.add(passwordInput);
    loginGroup.add(submitButton);
    
    // 统一取值
    console.log("===== 收集表单值 =====");
    console.log(loginGroup.getValue());
    
    // 统一校验
    console.log("\n===== 表单校验 =====");
    const isValid = loginGroup.validate();
    console.log(`📌 表单整体校验结果:${isValid ? "通过" : "失败"}`);
    

文件 / 文件夹管理(前端模拟网盘)

  1. 场景说明:网盘 / 文件管理器中,“文件” 是叶子节点 (无子节点)“文件夹” 是容器节点 (可包含文件 / 子文件夹),需要统一计算大小、统一删除、统一展示;
  2. 实现代码:
    // 1. 抽象组件
    abstract class FileSystemComponent {
      protected name: string;
      constructor(name: string) {
        this.name = name;
      }
    
      abstract getSize(): number; // 统一计算大小
      abstract del(): void; // 统一删除
      abstract display(indent: number): void; // 统一展示(带缩进)
    }
    
    // 2. 叶子节点:文件
    class myFile extends FileSystemComponent {
      private size: number; // 单位:KB
      constructor(name: string, size: number) {
        super(name);
        this.size = size;
      }
    
      getSize(): number {
        return this.size;
      }
    
      del(): void {
        console.log(`🗑️ 删除文件:${this.name}(${this.size}KB)`);
      }
    
      display(indent: number): void {
        console.log(`${"  ".repeat(indent)}📄 ${this.name} (${this.size}KB)`);
      }
    }
    
    // 3. 容器节点:文件夹
    class Folder extends FileSystemComponent {
      private children: FileSystemComponent[] = [];
      constructor(name: string) {
        super(name);
      }
    
      add(component: FileSystemComponent): void {
        this.children.push(component);
      }
    
      getSize(): number {
        // 递归计算所有子节点大小之和
        return this.children.reduce((total, child) => total + child.getSize(), 0);
      }
    
      del(): void {
        console.log(`🗑️ 删除文件夹:${this.name}(包含 ${this.children.length} 个子项)`);
        this.children.forEach(child => child.del()); // 递归删除子节点
      }
    
      display(indent: number): void {
        console.log(`${"  ".repeat(indent)}📁 ${this.name} (总大小:${this.getSize()}KB)`);
        this.children.forEach(child => child.display(indent + 1));
      }
    }
    
    // 4. 客户端使用
    const doc1 = new myFile("简历.docx", 200);
    const doc2 = new myFile("笔记.md", 50);
    const pic1 = new myFile("头像.png", 100);
    
    const docFolder = new Folder("文档");
    docFolder.add(doc1);
    docFolder.add(doc2);
    
    const rootFolder = new Folder("我的网盘");
    rootFolder.add(docFolder);
    rootFolder.add(pic1);
    
    // 统一展示
    console.log("===== 展示文件结构 =====");
    rootFolder.display(0);
    
    // 统一计算大小
    console.log(`\n===== 网盘总大小 ====="`);
    console.log(`我的网盘总大小:${rootFolder.getSize()}KB`);
    
    // 统一删除
    console.log(`\n===== 删除网盘 ====="`);
    rootFolder.del();
    

可视化画布元素(嵌套图形)

  1. 场景说明:Canvas/SVG 画布中,可能有 “组合图形(如仪表盘)→ 基础图形(如矩形、圆形)” 的嵌套结构,需要统一渲染、统一移动、统一计算面积;
  2. 实现代码:
    // 1. 抽象组件
    abstract class Graphic {
      protected x: number;
      protected y: number;
      constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
      }
    
      abstract render(): void; // 统一渲染
      abstract move(dx: number, dy: number): void; // 统一移动
      abstract getArea(): number; // 统一计算面积
    }
    
    // 2. 叶子节点:圆形
    class Circle extends Graphic {
      private radius: number;
      constructor(x: number, y: number, radius: number) {
        super(x, y);
        this.radius = radius;
      }
    
      render(): void {
        console.log(`🎨 渲染圆形:位置(${this.x},${this.y}),半径${this.radius}`);
      }
    
      move(dx: number, dy: number): void {
        this.x += dx;
        this.y += dy;
        console.log(`🚗 移动圆形至:(${this.x},${this.y})`);
      }
    
      getArea(): number {
        const area = Math.PI * this.radius * this.radius;
        console.log(`📏 圆形面积:${area.toFixed(2)}`);
        return area;
      }
    }
    
    // 叶子节点:矩形
    class Rectangle extends Graphic {
      private width: number;
      private height: number;
      constructor(x: number, y: number, width: number, height: number) {
        super(x, y);
        this.width = width;
        this.height = height;
      }
    
      render(): void {
        console.log(`🎨 渲染矩形:位置(${this.x},${this.y}),宽${this.width}高${this.height}`);
      }
    
      move(dx: number, dy: number): void {
        this.x += dx;
        this.y += dy;
        console.log(`🚗 移动矩形至:(${this.x},${this.y})`);
      }
    
      getArea(): number {
        const area = this.width * this.height;
        console.log(`📏 矩形面积:${area}`);
        return area;
      }
    }
    
    // 3. 容器节点:组合图形
    class CompositeGraphic extends Graphic {
      private children: Graphic[] = [];
      constructor(x: number, y: number) {
        super(x, y);
      }
    
      add(graphic: Graphic): void {
        this.children.push(graphic);
      }
    
      render(): void {
        console.log(`🎨 渲染组合图形:位置(${this.x},${this.y})`);
        this.children.forEach(child => child.render());
      }
    
      move(dx: number, dy: number): void {
        this.x += dx;
        this.y += dy;
        console.log(`🚗 移动组合图形至:(${this.x},${this.y})`);
        this.children.forEach(child => child.move(dx, dy)); // 递归移动子图形
      }
    
      getArea(): number {
        const totalArea = this.children.reduce((total, child) => total + child.getArea(), 0);
        console.log(`📏 组合图形总面积:${totalArea.toFixed(2)}`);
        return totalArea;
      }
    }
    
    // 4. 客户端使用
    const circle = new Circle(10, 10, 5);
    const rect = new Rectangle(20, 20, 10, 8);
    
    const dashboard = new CompositeGraphic(0, 0);
    dashboard.add(circle);
    dashboard.add(rect);
    
    // 统一渲染
    console.log("===== 渲染画布 =====");
    dashboard.render();
    
    // 统一计算面积
    console.log("\n===== 计算总面积 =====");
    dashboard.getArea();
    
    // 统一移动
    console.log("\n===== 移动所有图形 =====");
    dashboard.move(5, 5);
    

组合模式的优缺点

  1. 优点:
    1. 简化客户端代码:客户端可以一致地处理复杂树结构和简单叶子节点;
    2. 易于扩展:新增组件类型很方便;
    3. 符合开闭原则:无需修改现有代码就能添加新组件;
  2. 缺点:
    1. 过度通用:可能会使设计变得过于复杂;
    2. 类型安全:有时难以在编译时限制某些操作 (如对叶子节点执行add操作)
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 组合模式深度解析
  2. 2. 常用使用场景
    1. 2.1. 表单组件嵌套(表单校验 / 值收集)
    2. 2.2. 文件 / 文件夹管理(前端模拟网盘)
    3. 2.3. 可视化画布元素(嵌套图形)
  3. 3. 组合模式的优缺点