组合模式 (Composite Pattern)
NOTE
组合模式将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
📖 模式定义
组合模式是一种结构型设计模式,它将对象组合成树形结构来表现"整体/部分"层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
核心要素
- 组件接口:定义叶子和容器对象的公共接口
- 叶子节点:树形结构中的叶子节点,没有子节点
- 容器节点:有子节点的节点,实现与子节点相关的操作
- 客户端:通过组件接口操作组合结构中的对象
🎯 使用场景
适用情况
- 树形结构:需要表示对象的部分-整体层次结构
- 统一处理:希望用户忽略组合对象与单个对象的不同
- 递归结构:结构中的对象有相似的操作
- 层次遍历:需要遍历整个层次结构
不适用情况
- 对象结构过于简单
- 不需要统一处理单个对象和组合对象
- 性能要求极高的场景
💡 实现方式
TypeScript 实现
typescript
// 组件接口
interface FileSystemComponent {
getName(): string;
getSize(): number;
display(indent?: string): void;
add?(component: FileSystemComponent): void;
remove?(component: FileSystemComponent): void;
getChild?(index: number): FileSystemComponent | null;
}
// 叶子节点 - 文件
class File implements FileSystemComponent {
private name: string;
private size: number;
constructor(name: string, size: number) {
this.name = name;
this.size = size;
}
getName(): string {
return this.name;
}
getSize(): number {
return this.size;
}
display(indent: string = ''): void {
console.log(`${indent}📄 ${this.name} (${this.size} bytes)`);
}
}
// 容器节点 - 文件夹
class Directory implements FileSystemComponent {
private name: string;
private children: FileSystemComponent[] = [];
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
getSize(): number {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
display(indent: string = ''): void {
console.log(`${indent}📁 ${this.name}/ (${this.getSize()} bytes)`);
this.children.forEach(child => {
child.display(indent + ' ');
});
}
add(component: FileSystemComponent): void {
this.children.push(component);
}
remove(component: FileSystemComponent): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
getChild(index: number): FileSystemComponent | null {
return this.children[index] || null;
}
getChildren(): FileSystemComponent[] {
return [...this.children];
}
find(name: string): FileSystemComponent | null {
if (this.name === name) {
return this;
}
for (const child of this.children) {
if (child.getName() === name) {
return child;
}
if (child instanceof Directory) {
const found = child.find(name);
if (found) {
return found;
}
}
}
return null;
}
}
// 使用示例
console.log('=== Composite Pattern Demo ===');
// 创建文件系统结构
const root = new Directory('root');
const documents = new Directory('Documents');
const pictures = new Directory('Pictures');
const music = new Directory('Music');
// 添加文件
const resume = new File('resume.pdf', 1024);
const photo1 = new File('vacation.jpg', 2048);
const photo2 = new File('family.png', 1536);
const song1 = new File('favorite.mp3', 4096);
const song2 = new File('classical.wav', 8192);
// 构建目录结构
documents.add(resume);
pictures.add(photo1);
pictures.add(photo2);
music.add(song1);
music.add(song2);
root.add(documents);
root.add(pictures);
root.add(music);
// 显示整个文件系统
console.log('\n--- File System Structure ---');
root.display();
// 查找文件
console.log('\n--- Finding Files ---');
const foundFile = root.find('vacation.jpg');
if (foundFile) {
console.log(`Found: ${foundFile.getName()}`);
}
const foundDir = root.find('Music');
if (foundDir) {
console.log(`Found directory: ${foundDir.getName()}`);
foundDir.display();
}
高级组合模式示例
typescript
// 图形组件接口
interface GraphicComponent {
draw(): void;
move(x: number, y: number): void;
getBounds(): { x: number; y: number; width: number; height: number };
clone(): GraphicComponent;
}
// 基础图形类
abstract class BaseGraphic implements GraphicComponent {
protected x: number;
protected y: number;
protected width: number;
protected height: number;
constructor(x: number, y: number, width: number, height: number) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
move(x: number, y: number): void {
this.x = x;
this.y = y;
}
getBounds(): { x: number; y: number; width: number; height: number } {
return { x: this.x, y: this.y, width: this.width, height: this.height };
}
abstract draw(): void;
abstract clone(): GraphicComponent;
}
// 叶子节点 - 圆形
class Circle extends BaseGraphic {
private radius: number;
constructor(x: number, y: number, radius: number) {
super(x, y, radius * 2, radius * 2);
this.radius = radius;
}
draw(): void {
console.log(`🔵 Drawing circle at (${this.x}, ${this.y}) with radius ${this.radius}`);
}
clone(): Circle {
return new Circle(this.x, this.y, this.radius);
}
}
// 叶子节点 - 矩形
class Rectangle extends BaseGraphic {
draw(): void {
console.log(`🟦 Drawing rectangle at (${this.x}, ${this.y}) with size ${this.width}x${this.height}`);
}
clone(): Rectangle {
return new Rectangle(this.x, this.y, this.width, this.height);
}
}
// 叶子节点 - 文本
class Text extends BaseGraphic {
private content: string;
constructor(x: number, y: number, content: string) {
super(x, y, content.length * 8, 16); // 假设字符宽度
this.content = content;
}
draw(): void {
console.log(`📝 Drawing text "${this.content}" at (${this.x}, ${this.y})`);
}
clone(): Text {
return new Text(this.x, this.y, this.content);
}
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
this.width = content.length * 8;
}
}
// 容器节点 - 图形组
class GraphicGroup implements GraphicComponent {
private children: GraphicComponent[] = [];
private name: string;
constructor(name: string = 'Group') {
this.name = name;
}
add(component: GraphicComponent): void {
this.children.push(component);
}
remove(component: GraphicComponent): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
draw(): void {
console.log(`📦 Drawing group: ${this.name}`);
this.children.forEach((child, index) => {
console.log(` [${index + 1}]`);
child.draw();
});
}
move(x: number, y: number): void {
const bounds = this.getBounds();
const deltaX = x - bounds.x;
const deltaY = y - bounds.y;
this.children.forEach(child => {
const childBounds = child.getBounds();
child.move(childBounds.x + deltaX, childBounds.y + deltaY);
});
}
getBounds(): { x: number; y: number; width: number; height: number } {
if (this.children.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
}
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;
this.children.forEach(child => {
const bounds = child.getBounds();
minX = Math.min(minX, bounds.x);
minY = Math.min(minY, bounds.y);
maxX = Math.max(maxX, bounds.x + bounds.width);
maxY = Math.max(maxY, bounds.y + bounds.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
}
clone(): GraphicGroup {
const clonedGroup = new GraphicGroup(this.name + ' (Copy)');
this.children.forEach(child => {
clonedGroup.add(child.clone());
});
return clonedGroup;
}
getChildren(): GraphicComponent[] {
return [...this.children];
}
getName(): string {
return this.name;
}
setName(name: string): void {
this.name = name;
}
}
// 图形编辑器
class GraphicEditor {
private canvas: GraphicComponent[] = [];
private selectedComponents: GraphicComponent[] = [];
addComponent(component: GraphicComponent): void {
this.canvas.push(component);
console.log(`➕ Added component to canvas`);
}
removeComponent(component: GraphicComponent): void {
const index = this.canvas.indexOf(component);
if (index !== -1) {
this.canvas.splice(index, 1);
console.log(`➖ Removed component from canvas`);
}
}
selectComponent(component: GraphicComponent): void {
if (!this.selectedComponents.includes(component)) {
this.selectedComponents.push(component);
console.log(`✅ Selected component`);
}
}
deselectAll(): void {
this.selectedComponents = [];
console.log(`❌ Deselected all components`);
}
groupSelected(groupName: string = 'New Group'): GraphicGroup | null {
if (this.selectedComponents.length < 2) {
console.log(`⚠️ Need at least 2 components to create a group`);
return null;
}
const group = new GraphicGroup(groupName);
// 将选中的组件添加到组中,并从画布中移除
this.selectedComponents.forEach(component => {
group.add(component);
this.removeComponent(component);
});
// 将组添加到画布
this.addComponent(group);
// 清空选择
this.deselectAll();
console.log(`🔗 Created group: ${groupName}`);
return group;
}
ungroupSelected(): void {
const groupsToUngroup = this.selectedComponents.filter(
component => component instanceof GraphicGroup
) as GraphicGroup[];
groupsToUngroup.forEach(group => {
// 将组中的子组件添加到画布
group.getChildren().forEach(child => {
this.addComponent(child);
});
// 从画布中移除组
this.removeComponent(group);
});
this.deselectAll();
console.log(`🔓 Ungrouped ${groupsToUngroup.length} groups`);
}
drawCanvas(): void {
console.log('\n🎨 Drawing Canvas:');
console.log('==================');
this.canvas.forEach((component, index) => {
console.log(`[${index + 1}]`);
component.draw();
console.log('');
});
}
getCanvasComponents(): GraphicComponent[] {
return [...this.canvas];
}
}
// 使用示例
async function demonstrateGraphicComposite() {
console.log('\n=== Advanced Composite Pattern Demo ===');
const editor = new GraphicEditor();
// 创建基础图形
const circle1 = new Circle(10, 10, 5);
const circle2 = new Circle(30, 30, 8);
const rect1 = new Rectangle(50, 50, 20, 15);
const rect2 = new Rectangle(80, 80, 25, 10);
const text1 = new Text(100, 100, 'Hello World');
// 添加到画布
editor.addComponent(circle1);
editor.addComponent(circle2);
editor.addComponent(rect1);
editor.addComponent(rect2);
editor.addComponent(text1);
console.log('\n--- Initial Canvas ---');
editor.drawCanvas();
// 选择一些组件并创建组
editor.selectComponent(circle1);
editor.selectComponent(rect1);
const group1 = editor.groupSelected('Shapes Group');
editor.selectComponent(circle2);
editor.selectComponent(text1);
const group2 = editor.groupSelected('Mixed Group');
console.log('\n--- After Grouping ---');
editor.drawCanvas();
// 创建嵌套组
if (group1 && group2) {
editor.selectComponent(group1);
editor.selectComponent(group2);
editor.selectComponent(rect2);
const masterGroup = editor.groupSelected('Master Group');
console.log('\n--- After Creating Master Group ---');
editor.drawCanvas();
// 移动整个组
if (masterGroup) {
console.log('\n--- Moving Master Group ---');
masterGroup.move(200, 200);
editor.drawCanvas();
// 克隆组
console.log('\n--- Cloning Master Group ---');
const clonedGroup = masterGroup.clone();
clonedGroup.move(400, 400);
editor.addComponent(clonedGroup);
editor.drawCanvas();
}
}
}
demonstrateGraphicComposite();
⚖️ 优缺点分析
✅ 优点
- 统一接口:客户端可以一致地处理单个对象和组合对象
- 简化客户端:客户端不需要区分叶子和容器对象
- 易于扩展:容易增加新的组件类型
- 递归结构:天然支持树形结构的递归操作
❌ 缺点
- 设计复杂:使设计变得更加抽象
- 类型安全:难以限制组合中组件的类型
- 性能开销:递归调用可能带来性能问题
🌟 实际应用案例
1. UI组件系统
typescript
// UI组件接口
interface UIComponent {
render(): string;
getSize(): { width: number; height: number };
setVisible(visible: boolean): void;
isVisible(): boolean;
addEventListener(event: string, handler: Function): void;
}
// 基础UI组件
abstract class BaseUIComponent implements UIComponent {
protected visible: boolean = true;
protected eventListeners: Map<string, Function[]> = new Map();
setVisible(visible: boolean): void {
this.visible = visible;
}
isVisible(): boolean {
return this.visible;
}
addEventListener(event: string, handler: Function): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event)!.push(handler);
}
protected triggerEvent(event: string, data?: any): void {
const handlers = this.eventListeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
abstract render(): string;
abstract getSize(): { width: number; height: number };
}
// 叶子组件 - 按钮
class Button extends BaseUIComponent {
private text: string;
private width: number;
private height: number;
constructor(text: string, width: number = 100, height: number = 30) {
super();
this.text = text;
this.width = width;
this.height = height;
}
render(): string {
if (!this.visible) return '';
return `<button style="width:${this.width}px;height:${this.height}px">${this.text}</button>`;
}
getSize(): { width: number; height: number } {
return { width: this.width, height: this.height };
}
setText(text: string): void {
this.text = text;
}
click(): void {
this.triggerEvent('click', { text: this.text });
}
}
// 叶子组件 - 输入框
class Input extends BaseUIComponent {
private placeholder: string;
private value: string = '';
private width: number;
private height: number;
constructor(placeholder: string, width: number = 200, height: number = 30) {
super();
this.placeholder = placeholder;
this.width = width;
this.height = height;
}
render(): string {
if (!this.visible) return '';
return `<input placeholder="${this.placeholder}" value="${this.value}" style="width:${this.width}px;height:${this.height}px">`;
}
getSize(): { width: number; height: number } {
return { width: this.width, height: this.height };
}
setValue(value: string): void {
this.value = value;
this.triggerEvent('change', { value });
}
getValue(): string {
return this.value;
}
}
// 叶子组件 - 标签
class Label extends BaseUIComponent {
private text: string;
private fontSize: number;
constructor(text: string, fontSize: number = 14) {
super();
this.text = text;
this.fontSize = fontSize;
}
render(): string {
if (!this.visible) return '';
return `<label style="font-size:${this.fontSize}px">${this.text}</label>`;
}
getSize(): { width: number; height: number } {
return {
width: this.text.length * this.fontSize * 0.6,
height: this.fontSize + 4
};
}
setText(text: string): void {
this.text = text;
}
}
// 容器组件 - 面板
class Panel extends BaseUIComponent {
private children: UIComponent[] = [];
private layout: 'vertical' | 'horizontal' = 'vertical';
private padding: number = 10;
private title?: string;
constructor(title?: string, layout: 'vertical' | 'horizontal' = 'vertical') {
super();
this.title = title;
this.layout = layout;
}
addComponent(component: UIComponent): void {
this.children.push(component);
}
removeComponent(component: UIComponent): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
render(): string {
if (!this.visible) return '';
const childrenHtml = this.children
.filter(child => child.isVisible())
.map(child => child.render())
.join('');
const flexDirection = this.layout === 'vertical' ? 'column' : 'row';
const titleHtml = this.title ? `<h3>${this.title}</h3>` : '';
return `
<div style="display:flex;flex-direction:${flexDirection};padding:${this.padding}px;border:1px solid #ccc;">
${titleHtml}
${childrenHtml}
</div>
`;
}
getSize(): { width: number; height: number } {
if (this.children.length === 0) {
return { width: this.padding * 2, height: this.padding * 2 };
}
const visibleChildren = this.children.filter(child => child.isVisible());
if (this.layout === 'vertical') {
const maxWidth = Math.max(...visibleChildren.map(child => child.getSize().width));
const totalHeight = visibleChildren.reduce((sum, child) => sum + child.getSize().height, 0);
return {
width: maxWidth + this.padding * 2,
height: totalHeight + this.padding * 2
};
} else {
const totalWidth = visibleChildren.reduce((sum, child) => sum + child.getSize().width, 0);
const maxHeight = Math.max(...visibleChildren.map(child => child.getSize().height));
return {
width: totalWidth + this.padding * 2,
height: maxHeight + this.padding * 2
};
}
}
getChildren(): UIComponent[] {
return [...this.children];
}
setLayout(layout: 'vertical' | 'horizontal'): void {
this.layout = layout;
}
}
// 表单组件
class Form extends Panel {
private formData: Map<string, any> = new Map();
constructor(title: string = 'Form') {
super(title, 'vertical');
}
addField(label: string, input: Input): void {
const fieldPanel = new Panel(undefined, 'horizontal');
const labelComponent = new Label(label);
fieldPanel.addComponent(labelComponent);
fieldPanel.addComponent(input);
// 监听输入变化
input.addEventListener('change', (data) => {
this.formData.set(label, data.value);
this.triggerEvent('fieldChange', { field: label, value: data.value });
});
this.addComponent(fieldPanel);
}
addButton(button: Button): void {
button.addEventListener('click', () => {
this.triggerEvent('buttonClick', {
button: button,
formData: Object.fromEntries(this.formData)
});
});
this.addComponent(button);
}
getFormData(): Record<string, any> {
return Object.fromEntries(this.formData);
}
reset(): void {
this.formData.clear();
// 重置所有输入框
this.resetInputs(this);
}
private resetInputs(component: UIComponent): void {
if (component instanceof Input) {
component.setValue('');
} else if (component instanceof Panel) {
component.getChildren().forEach(child => this.resetInputs(child));
}
}
}
// 使用示例
function demonstrateUIComposite() {
console.log('\n=== UI Composite Pattern Demo ===');
// 创建登录表单
const loginForm = new Form('Login Form');
const usernameInput = new Input('Enter username');
const passwordInput = new Input('Enter password');
const loginButton = new Button('Login');
const cancelButton = new Button('Cancel');
loginForm.addField('Username', usernameInput);
loginForm.addField('Password', passwordInput);
// 创建按钮面板
const buttonPanel = new Panel(undefined, 'horizontal');
buttonPanel.addComponent(loginButton);
buttonPanel.addComponent(cancelButton);
loginForm.addComponent(buttonPanel);
// 添加事件监听
loginForm.addEventListener('fieldChange', (data) => {
console.log(`Field changed: ${data.field} = ${data.value}`);
});
loginForm.addEventListener('buttonClick', (data) => {
if (data.button === loginButton) {
console.log('Login clicked with data:', data.formData);
} else if (data.button === cancelButton) {
console.log('Cancel clicked');
loginForm.reset();
}
});
// 渲染表单
console.log('\n--- Rendered Form ---');
console.log(loginForm.render());
// 模拟用户交互
console.log('\n--- User Interactions ---');
usernameInput.setValue('john_doe');
passwordInput.setValue('secret123');
loginButton.click();
console.log('\n--- Form Size ---');
const size = loginForm.getSize();
console.log(`Form size: ${size.width}x${size.height}`);
}
demonstrateUIComposite();
🔄 相关模式
与其他模式的关系
- 装饰器模式:都使用递归组合,但装饰器添加职责,组合表示层次结构
- 享元模式:可以与组合模式结合,共享叶子节点
- 迭代器模式:可以用来遍历组合结构
- 访问者模式:可以在组合结构上执行操作
🚀 最佳实践
- 接口设计:保持组件接口简单一致
- 类型安全:使用泛型或类型检查确保组件类型正确
- 性能优化:对于大型树结构,考虑缓存和懒加载
- 错误处理:处理循环引用和无效操作
- 内存管理:及时清理不需要的组件引用
⚠️ 注意事项
- 循环引用:避免在组合结构中创建循环引用
- 类型限制:考虑是否需要限制容器中的组件类型
- 性能考虑:深层嵌套可能影响性能
- 内存泄漏:注意清理事件监听器和引用
📚 总结
组合模式是处理树形结构的强大工具,它让客户端能够统一处理单个对象和对象组合。这个模式在UI系统、文件系统、组织结构等场景中非常有用。
使用建议:
- 当需要表示对象的部分-整体层次结构时使用
- 当希望用户忽略组合对象与单个对象的不同时使用
- 当结构中的对象有相似操作时使用
- 优先考虑接口的一致性和简洁性
相关链接: