组合模式深度解析
- 组合模式:
- 是一种结构型设计模式,核心思想是将对象组合成树形结构,以表示 “部分 - 整体” 的层次关系,让客户端能以统一的方式处理单个对象 (叶子节点) 和对象组合 (容器节点);
- 简单比喻:把 “文件” (叶子) 和 “文件夹” (容器) 统一视为 “文件系统节点”,不管点击文件还是文件夹,都能执行 “查看大小”、“删除” 等操作,无需区分类型;
- 类图:
- 实现代码:
// ---------------------- 抽象组件(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. 抽象组件 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. 抽象组件 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();
可视化画布元素(嵌套图形)
- 场景说明:Canvas/SVG 画布中,可能有 “组合图形(如仪表盘)→ 基础图形(如矩形、圆形)” 的嵌套结构,需要统一渲染、统一移动、统一计算面积;
- 实现代码:
// 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);
组合模式的优缺点
- 优点:
- 简化客户端代码:客户端可以一致地处理复杂树结构和简单叶子节点;
- 易于扩展:新增组件类型很方便;
- 符合开闭原则:无需修改现有代码就能添加新组件;
- 缺点:
- 过度通用:可能会使设计变得过于复杂;
- 类型安全:有时难以在编译时限制某些操作 (如对叶子节点执行add操作);
桥接模式(结构型模式)
上一篇