组合模式

组合模式(Composite Pattern)是结构型设计模式的核心成员之一,其核心设计思想是将对象组合成树形结构,用以表示“部分-整体”的层次关系,核心目标是让客户端对单个对象(叶子节点)和组合对象(容器节点)的访问具有一致性——无需区分两者类型,即可通过统一接口完成操作,从而简化客户端逻辑,提升系统的可扩展性与可维护性。

组合模式的核心价值在于“统一访问”与“层次化管理”,尤其适配具有明显层级结构的业务场景,它将复杂的树形结构封装为统一的组件接口,让客户端无需关注结构的复杂性,只需专注于业务逻辑的实现。

一、组合模式的核心结构

组合模式通过四个核心角色的分工协作,构建“部分-整体”的树形结构,实现单个对象与组合对象的统一访问,各角色职责清晰、边界明确,共同支撑模式的核心逻辑:

  • 抽象组件(Component):定义单个对象和组合对象的公共接口,是客户端与所有组件交互的统一入口。接口中需包含所有组件的通用操作(如遍历、计算、添加/删除子节点等),其中添加、删除等组合节点特有操作,可在叶子节点中做空实现或抛出异常,兼顾接口统一性与角色特殊性。

  • 叶子节点(Leaf):树形结构中的最小功能单元,无任何子节点,是“部分”的具体实现。它仅需实现抽象组件中与自身相关的核心操作,对于添加、删除子节点等组合节点特有操作,无需实现(或抛出不支持的异常)。

  • 组合节点(Composite):树形结构中的容器单元,可包含子节点(叶子节点或其他组合节点),是“整体”的具体实现。它不仅要实现抽象组件的所有通用操作,核心职责还包括管理子节点(添加、删除、遍历),并通过递归调用子节点的方法,完成整体功能的聚合。

  • 客户端(Client):通过抽象组件接口访问所有节点,无需区分叶子节点与组合节点,无需关注树形结构的层级细节,实现“一键操作”所有组件(单个或组合)。

组合模式的核心逻辑可概括为:树形结构 + 统一接口 + 递归遍历。其中,递归是组合节点处理子节点、聚合整体功能的核心方式,也是实现“统一访问”的关键——组合节点通过递归调用子节点的方法,将自身的操作传递给所有子节点,最终完成整体功能的计算或执行。

二、多语言实现组合模式

组合模式的核心是“树形结构的统一访问”,不同语言因语法特性差异,实现方式略有不同,但核心逻辑高度统一。以下基于“文件系统”这一经典场景(文件=叶子节点,文件夹=组合节点,支持遍历、计算大小等操作),实现C#、Python、Golang、C++、纯C五种语言的完整可运行案例,代码均添加规范注释,便于直接复用,同时贴合各语言的设计理念。

2.1 C# 实现(面向对象标准实现)

C# 作为强类型面向对象语言,通过抽象类定义抽象组件接口,明确所有组件的通用操作,叶子节点与组合节点分别继承抽象类,实现各自的核心逻辑,代码结构严谨、可维护性高,贴合企业级开发规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System;
using System.Collections.Generic;

// 抽象组件:文件系统节点,定义统一接口
public abstract class FileSystemComponent
{
protected string Name; // 节点名称

// 构造函数:初始化节点名称
public FileSystemComponent(string name) => Name = name;

// 公共操作:计算节点大小(核心通用操作)
public abstract long GetSize();

// 组合节点特有操作:添加子节点(叶子节点默认抛异常)
public virtual void Add(FileSystemComponent component) =>
throw new NotSupportedException($"{Name} 是叶子节点,不支持添加子节点");

// 组合节点特有操作:删除子节点(叶子节点默认抛异常)
public virtual void Remove(FileSystemComponent component) =>
throw new NotSupportedException($"{Name} 是叶子节点,不支持删除子节点");
}

// 叶子节点:文件(无子类节点,仅实现核心操作)
public class File : FileSystemComponent
{
private long _size; // 文件大小(单位:字节)

// 构造函数:初始化文件名称和大小
public File(string name, long size) : base(name) => _size = size;

// 实现计算大小的核心方法
public override long GetSize()
{
Console.WriteLine($"文件【{Name}】大小:{_size} 字节");
return _size;
}
}

// 组合节点:文件夹(包含子节点,管理子节点并聚合功能)
public class Folder : FileSystemComponent
{
// 存储子节点(支持叶子节点和其他组合节点)
private List<FileSystemComponent> _children = new List<FileSystemComponent>();

// 构造函数:初始化文件夹名称
public Folder(string name) : base(name) { }

// 实现添加子节点方法
public override void Add(FileSystemComponent component) => _children.Add(component);

// 实现删除子节点方法
public override void Remove(FileSystemComponent component) => _children.Remove(component);

// 实现计算大小方法:递归遍历子节点,聚合总大小
public override long GetSize()
{
long totalSize = 0;
Console.WriteLine($"文件夹【{Name}】包含的文件大小:");
// 递归调用所有子节点的GetSize方法,累加总大小
foreach (var component in _children)
{
totalSize += component.GetSize();
}
Console.WriteLine($"文件夹【{Name}】总大小:{totalSize} 字节\n");
return totalSize;
}
}

// 客户端调用:统一操作叶子节点和组合节点
class Program
{
static void Main()
{
// 构建文件系统树形结构
// 叶子节点:创建两个文件
File file1 = new File("笔记.txt", 1024); // 1KB
File file2 = new File("图片.png", 20480); // 20KB

// 组合节点1:创建“文档”文件夹,添加文件子节点
Folder folder1 = new Folder("文档");
folder1.Add(file1);
folder1.Add(file2);

// 叶子节点:创建视频文件
File file3 = new File("视频.mp4", 102400); // 100KB

// 组合节点2:创建“根目录”文件夹,添加子文件夹和文件
Folder root = new Folder("根目录");
root.Add(folder1);
root.Add(file3);

// 统一调用GetSize方法,无需区分叶子和组合节点
Console.WriteLine("=== 计算文件系统总大小 ===");
root.GetSize();
}
}

2.2 Python 实现(动态语言简洁实现)

Python 遵循“鸭子类型”,无需显式定义抽象类接口,通过基类模拟抽象组件,叶子节点与组合节点继承基类并实现对应方法,语法简洁灵活,无需繁琐的类型声明,适配快速开发与脚本场景,同时保留组合模式的核心逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class FileSystemComponent:
"""抽象组件(基类):定义文件系统节点的统一接口"""
def __init__(self, name):
self.name = name # 节点名称

def get_size(self):
"""公共方法:计算节点大小,子类必须实现"""
raise NotImplementedError("子类必须实现get_size方法,用于计算节点大小")

def add(self, component):
"""组合节点特有方法:添加子节点,叶子节点默认抛出异常"""
raise NotImplementedError(f"{self.name} 是叶子节点,不支持添加子节点")

def remove(self, component):
"""组合节点特有方法:删除子节点,叶子节点默认抛出异常"""
raise NotImplementedError(f"{self.name} 是叶子节点,不支持删除子节点")

class File(FileSystemComponent):
"""叶子节点:文件,无子类节点,实现核心计算逻辑"""
def __init__(self, name, size):
super().__init__(name)
self.size = size # 文件大小(单位:字节)

def get_size(self):
"""实现计算文件大小的方法"""
print(f"文件【{self.name}】大小:{self.size} 字节")
return self.size

class Folder(FileSystemComponent):
"""组合节点:文件夹,管理子节点并聚合计算总大小"""
def __init__(self, name):
super().__init__(name)
self.children = [] # 存储子节点(文件或文件夹)

def add(self, component):
"""实现添加子节点的方法"""
self.children.append(component)

def remove(self, component):
"""实现删除子节点的方法"""
self.children.remove(component)

def get_size(self):
"""实现计算文件夹总大小的方法:递归遍历子节点"""
total_size = 0
print(f"文件夹【{self.name}】包含的文件大小:")
for child in self.children:
total_size += child.get_size() # 递归调用子节点的get_size
print(f"文件夹【{self.name}】总大小:{total_size} 字节\n")
return total_size

# 客户端调用:统一操作所有节点
if __name__ == "__main__":
# 构建文件系统树形结构
file1 = File("笔记.txt", 1024)
file2 = File("图片.png", 20480)
folder1 = Folder("文档")
folder1.add(file1)
folder1.add(file2)

file3 = File("视频.mp4", 102400)
root = Folder("根目录")
root.add(folder1)
root.add(file3)

# 统一调用get_size,无需区分叶子和组合节点
print("=== 计算文件系统总大小 ===")
root.get_size()

2.3 Go 实现(组合优于继承的极简实现)

Go 语言无类和继承概念,核心遵循“组合优于继承”的设计哲学,通过接口定义抽象组件的统一规范,结构体分别实现接口方法(叶子节点与组合节点),无需继承,代码极简、高效,贴合Go语言的设计理念,同时完美实现组合模式的核心逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package main

import (
"fmt"
)

// 抽象组件:FileSystemComponent接口,定义统一操作规范
type FileSystemComponent interface {
GetSize() int64 // 公共方法:计算节点大小
Add(component FileSystemComponent) error // 组合节点特有:添加子节点
Remove(component FileSystemComponent) error // 组合节点特有:删除子节点
}

// 叶子节点:File结构体,实现FileSystemComponent接口
type File struct {
name string // 文件名称
size int64 // 文件大小(单位:字节)
}

// 工厂方法:创建文件节点
func NewFile(name string, size int64) *File {
return &File{name: name, size: size}
}

// 实现GetSize方法:计算文件大小
func (f *File) GetSize() int64 {
fmt.Printf("文件【%s】大小:%d 字节\n", f.name, f.size)
return f.size
}

// 实现Add方法:叶子节点不支持添加子节点,返回错误
func (f *File) Add(component FileSystemComponent) error {
return fmt.Errorf("文件【%s】不支持添加子节点", f.name)
}

// 实现Remove方法:叶子节点不支持删除子节点,返回错误
func (f *File) Remove(component FileSystemComponent) error {
return fmt.Errorf("文件【%s】不支持删除子节点", f.name)
}

// 组合节点:Folder结构体,实现FileSystemComponent接口
type Folder struct {
name string // 文件夹名称
children []FileSystemComponent // 存储子节点(文件或文件夹)
}

// 工厂方法:创建文件夹节点
func NewFolder(name string) *Folder {
return &Folder{name: name, children: make([]FileSystemComponent, 0)}
}

// 实现Add方法:添加子节点
func (f *Folder) Add(component FileSystemComponent) error {
f.children = append(f.children, component)
return nil
}

// 实现Remove方法:删除子节点
func (f *Folder) Remove(component FileSystemComponent) error {
// 遍历查找子节点,找到后删除
for i, c := range f.children {
if c == component {
f.children = append(f.children[:i], f.children[i+1:]...)
return nil
}
}
return fmt.Errorf("子节点不存在,删除失败")
}

// 实现GetSize方法:递归遍历子节点,计算总大小
func (f *Folder) GetSize() int64 {
var totalSize int64 = 0
fmt.Printf("文件夹【%s】包含的文件大小:\n", f.name)
for _, child := range f.children {
totalSize += child.GetSize() // 递归调用子节点的GetSize
}
fmt.Printf("文件夹【%s】总大小:%d 字节\n\n", f.name, totalSize)
return totalSize
}

// 客户端调用:统一操作所有节点
func main() {
// 构建文件系统树形结构
file1 := NewFile("笔记.txt", 1024)
file2 := NewFile("图片.png", 20480)
folder1 := NewFolder("文档")
folder1.Add(file1)
folder1.Add(file2)

file3 := NewFile("视频.mp4", 102400)
root := NewFolder("根目录")
root.Add(folder1)
root.Add(file3)

// 统一调用GetSize,无需区分叶子和组合节点
fmt.Println("=== 计算文件系统总大小 ===")
root.GetSize()
}

2.4 C++ 实现(面向对象经典实现)

C++ 作为经典面向对象语言,通过抽象类(纯虚函数)定义抽象组件接口,明确通用操作规范,叶子节点与组合节点继承抽象类并实现对应方法,依托多态特性实现统一访问,兼顾灵活性与性能,适配底层开发与高性能场景,同时完善内存管理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>

// 抽象组件:FileSystemComponent抽象类,定义统一接口
class FileSystemComponent {
protected:
std::string name; // 节点名称
public:
// 构造函数:初始化节点名称
FileSystemComponent(const std::string& name) : name(name) {}
// 虚析构函数:避免析构时内存泄漏(多态场景必备)
virtual ~FileSystemComponent() = default;

// 纯虚函数:计算节点大小(核心通用操作,子类必须实现)
virtual long long getSize() = 0;

// 虚函数:添加子节点(组合节点实现,叶子节点抛异常)
virtual void add(FileSystemComponent* component) {
throw std::runtime_error(name + " 是叶子节点,不支持添加子节点");
}

// 虚函数:删除子节点(组合节点实现,叶子节点抛异常)
virtual void remove(FileSystemComponent* component) {
throw std::runtime_error(name + " 是叶子节点,不支持删除子节点");
}
};

// 叶子节点:File类,继承FileSystemComponent
class File : public FileSystemComponent {
private:
long long size; // 文件大小(单位:字节)
public:
// 构造函数:初始化文件名称和大小
File(const std::string& name, long long size) : FileSystemComponent(name), size(size) {}

// 实现getSize方法:计算文件大小
long long getSize() override {
std::cout << "文件【" << name << "】大小:" << size << " 字节" << std::endl;
return size;
}
};

// 组合节点:Folder类,继承FileSystemComponent
class Folder : public FileSystemComponent {
private:
// 存储子节点指针(支持File和Folder)
std::vector<FileSystemComponent*> children;
public:
// 构造函数:初始化文件夹名称
Folder(const std::string& name) : FileSystemComponent(name) {}

// 析构函数:递归释放所有子节点内存,避免内存泄漏
~Folder() override {
for (auto child : children) {
delete child;
}
}

// 实现add方法:添加子节点
void add(FileSystemComponent* component) override {
children.push_back(component);
}

// 实现remove方法:删除子节点并释放内存
void remove(FileSystemComponent* component) override {
for (auto it = children.begin(); it != children.end(); ++it) {
if (*it == component) {
children.erase(it);
delete component; // 释放子节点内存
break;
}
}
}

// 实现getSize方法:递归遍历子节点,计算总大小
long long getSize() override {
long long totalSize = 0;
std::cout << "文件夹【" << name << "】包含的文件大小:" << std::endl;
for (auto child : children) {
totalSize += child->getSize(); // 递归调用子节点的getSize
}
std::cout << "文件夹【" << name << "】总大小:" << totalSize << " 字节" << std::endl << std::endl;
return totalSize;
}
};

// 客户端调用:统一操作叶子节点和组合节点
int main() {
// 构建文件系统树形结构
FileSystemComponent* file1 = new File("笔记.txt", 1024);
FileSystemComponent* file2 = new File("图片.png", 20480);
Folder* folder1 = new Folder("文档");
folder1->add(file1);
folder1->add(file2);

FileSystemComponent* file3 = new File("视频.mp4", 102400);
Folder* root = new Folder("根目录");
root->add(folder1);
root->add(file3);

// 统一调用getSize方法,无需区分叶子和组合节点
std::cout << "=== 计算文件系统总大小 ===" << std::endl;
root->getSize();

// 释放根目录内存(递归释放所有子节点)
delete root;
return 0;
}

2.5 纯C语言实现(结构体+函数指针模拟实现)

纯C语言无面向对象特性,无类和多态,通过“结构体封装数据+函数指针模拟接口+类型标记区分节点类型”,手动模拟组合模式的核心逻辑,实现叶子节点与组合节点的统一访问。代码虽冗余,但底层可控,适配嵌入式、底层开发等资源受限场景,同时完善内存管理,避免内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 组件类型枚举:标记节点是文件(叶子)还是文件夹(组合)
typedef enum {
TYPE_FILE, // 叶子节点:文件
TYPE_FOLDER // 组合节点:文件夹
} ComponentType;

// 前向声明:解决结构体之间的循环引用问题
typedef struct FileSystemComponent FileSystemComponent;

// 抽象组件的函数指针类型(模拟接口方法)
typedef long long (*GetSizeFunc)(FileSystemComponent*); // 计算大小
typedef int (*AddFunc)(FileSystemComponent*, FileSystemComponent*); // 添加子节点
typedef int (*RemoveFunc)(FileSystemComponent*, FileSystemComponent*); // 删除子节点

// 抽象组件结构体(模拟接口)
struct FileSystemComponent {
ComponentType type; // 节点类型标记
char name[256]; // 节点名称(最大255个字符)
GetSizeFunc get_size; // 计算大小函数指针
AddFunc add; // 添加子节点函数指针
RemoveFunc remove; // 删除子节点函数指针
};

// 叶子节点:File结构体(嵌套抽象组件,模拟“继承”)
typedef struct {
FileSystemComponent base; // 基组件(抽象接口)
long long size; // 文件大小(单位:字节)
} File;

// 组合节点:Folder结构体(嵌套抽象组件,模拟“继承”)
typedef struct {
FileSystemComponent base; // 基组件(抽象接口)
FileSystemComponent** children; // 子节点数组(存储文件或文件夹指针)
int count; // 当前子节点数量
int capacity; // 子节点数组容量(动态扩容)
} Folder;

// -------------------------- 叶子节点(File)方法实现 --------------------------
// 计算文件大小
long long file_get_size(FileSystemComponent* component) {
File* file = (File*)component; // 类型转换:基组件 -> 文件节点
printf("文件【%s】大小:%lld 字节\n", file->base.name, file->size);
return file->size;
}

// 叶子节点添加子节点(不支持,返回错误)
int file_add(FileSystemComponent* component, FileSystemComponent* child) {
fprintf(stderr, "错误:文件【%s】是叶子节点,不支持添加子节点\n", component->name);
return -1; // 返回-1表示失败
}

// 叶子节点删除子节点(不支持,返回错误)
int file_remove(FileSystemComponent* component, FileSystemComponent* child) {
fprintf(stderr, "错误:文件【%s】是叶子节点,不支持删除子节点\n", component->name);
return -1; // 返回-1表示失败
}

// 工厂函数:创建文件节点(初始化函数指针和数据)
FileSystemComponent* create_file(const char* name, long long size) {
File* file = (File*)malloc(sizeof(File));
if (file == NULL) return NULL; // 内存分配失败

// 初始化基组件
file->base.type = TYPE_FILE;
strncpy(file->base.name, name, sizeof(file->base.name) - 1); // 避免越界
file->base.get_size = file_get_size;
file->base.add = file_add;
file->base.remove = file_remove;

// 初始化文件特有属性
file->size = size;
return (FileSystemComponent*)file; // 转换为基组件指针,统一返回
}

// -------------------------- 组合节点(Folder)方法实现 --------------------------
// 计算文件夹总大小:递归遍历子节点
long long folder_get_size(FileSystemComponent* component) {
Folder* folder = (Folder*)component; // 类型转换:基组件 -> 文件夹节点
long long total_size = 0;
printf("文件夹【%s】包含的文件大小:\n", folder->base.name);

// 递归调用所有子节点的get_size方法
for (int i = 0; i < folder->count; i++) {
total_size += folder->children[i]->get_size(folder->children[i]);
}

printf("文件夹【%s】总大小:%lld 字节\n\n", folder->base.name, total_size);
return total_size;
}

// 文件夹添加子节点(支持动态扩容)
int folder_add(FileSystemComponent* component, FileSystemComponent* child) {
Folder* folder = (Folder*)component;

// 动态扩容:当子节点数量达到容量时,扩容为原来的2倍
if (folder->count >= folder->capacity) {
folder->capacity = (folder->capacity == 0) ? 4 : folder->capacity * 2;
folder->children = (FileSystemComponent**)realloc(
folder->children, sizeof(FileSystemComponent*) * folder->capacity
);
if (folder->children == NULL) return -1; // 扩容失败
}

// 添加子节点
folder->children[folder->count++] = child;
return 0; // 返回0表示成功
}

// 文件夹删除子节点(释放子节点内存)
int folder_remove(FileSystemComponent* component, FileSystemComponent* child) {
Folder* folder = (Folder*)component;

// 遍历查找子节点
for (int i = 0; i < folder->count; i++) {
if (folder->children[i] == child) {
// 移除子节点:移动后续元素,覆盖当前位置
for (int j = i; j < folder->count - 1; j++) {
folder->children[j] = folder->children[j + 1];
}
folder->count--;
free(child); // 释放被删除子节点的内存
return 0; // 成功
}
}

fprintf(stderr, "错误:子节点不存在,删除失败\n");
return -1; // 失败
}

// 工厂函数:创建文件夹节点(初始化函数指针和数据)
FileSystemComponent* create_folder(const char* name) {
Folder* folder = (Folder*)malloc(sizeof(Folder));
if (folder == NULL) return NULL; // 内存分配失败

// 初始化基组件
folder->base.type = TYPE_FOLDER;
strncpy(folder->base.name, name, sizeof(folder->base.name) - 1);
folder->base.get_size = folder_get_size;
folder->base.add = folder_add;
folder->base.remove = folder_remove;

// 初始化文件夹特有属性
folder->children = NULL;
folder->count = 0;
folder->capacity = 0;
return (FileSystemComponent*)folder; // 转换为基组件指针,统一返回
}

// 递归释放节点内存(避免内存泄漏)
void free_component(FileSystemComponent* component) {
if (component == NULL) return;

// 如果是文件夹,先递归释放所有子节点
if (component->type == TYPE_FOLDER) {
Folder* folder = (Folder*)component;
for (int i = 0; i < folder->count; i++) {
free_component(folder->children[i]);
}
free(folder->children); // 释放子节点数组内存
}

free(component); // 释放当前节点内存
}

// -------------------------- 客户端调用 --------------------------
int main() {
// 构建文件系统树形结构
FileSystemComponent* file1 = create_file("笔记.txt", 1024);
FileSystemComponent* file2 = create_file("图片.png", 20480);
FileSystemComponent* folder1 = create_folder("文档");
folder1->add(folder1, file1);
folder1->add(folder1, file2);

FileSystemComponent* file3 = create_file("视频.mp4", 102400);
FileSystemComponent* root = create_folder("根目录");
root->add(root, folder1);
root->add(root, file3);

// 统一调用get_size方法,无需区分叶子和组合节点
printf("=== 计算文件系统总大小 ===\n");
root->get_size(root);

// 递归释放所有节点内存,避免内存泄漏
free_component(root);
return 0;
}

三、组合模式的优缺点

组合模式的核心价值是“统一访问接口、简化树形结构操作”,其优缺点均围绕这一核心展开,需结合业务场景的层级复杂度、扩展需求,权衡使用,避免过度设计或滥用。

3.1 核心优点

  • 客户端访问一致性:客户端无需区分叶子节点与组合节点,统一通过抽象组件接口操作,无需关注“部分”与“整体”的差异,大幅简化客户端代码逻辑,降低使用成本。

  • 系统扩展性强:新增叶子节点或组合节点时,只需实现抽象组件接口,无需修改现有代码(完全符合开闭原则),可快速扩展树形结构的层级和功能。

  • 树形结构天然适配:完美契合“部分-整体”的层次化业务场景(如文件系统、菜单树、组织架构),代码结构与业务结构高度一致,提升代码的可读性和可维护性。

  • 简化聚合操作:组合节点通过递归遍历子节点,可轻松实现整体功能的聚合(如计算总大小、批量删除、批量更新),无需手动遍历整个树形结构。

3.2 主要缺点

  • 接口设计难度高:抽象组件接口需兼顾叶子节点与组合节点的操作,若接口包含过多组合节点特有的操作(如add/remove),叶子节点需做空实现或抛出异常,违反接口隔离原则,增加接口设计的复杂度。

  • 性能开销明显:组合节点的功能实现依赖递归遍历,对于深层、庞大的树形结构,递归调用会带来一定的性能损耗,且可能出现栈溢出风险。

  • 内存管理复杂:无垃圾回收机制的语言(如C语言),需手动递归释放树形结构的所有节点,易出现内存泄漏,增加开发和维护成本。

  • 子节点类型限制困难:若需限制组合节点的子节点类型(如某文件夹只能包含特定类型的文件),需在add方法中增加类型判断逻辑,增加代码复杂度,且违背“统一接口”的设计初衷。

四、组合模式的使用场景

组合模式的核心适用场景是“存在‘部分-整体’层次关系、客户端需统一操作单个对象与组合对象”,以下是具体场景及典型实战案例,便于快速落地应用:

  • 树形层次结构场景:业务场景天然呈现树形层级,需表示“部分-整体”关系,如文件系统(文件=叶子,文件夹=组合)、菜单树(菜单项=叶子,菜单组=组合)、组织架构树(员工=叶子,部门=组合)、XML/JSON节点树、UI组件树(按钮=叶子,面板=组合)。

  • 客户端统一操作场景:希望客户端以相同方式处理单个对象和组合对象,无需区分类型,如批量计算树形结构中所有节点的数值(如文件总大小、菜单总数量)、批量遍历所有节点、批量执行某一操作(如批量删除文件、批量禁用菜单)。

  • 动态扩展结构场景:树形结构的层级和节点数量不固定,需频繁添加、删除子节点,且扩展时无需修改现有代码,如动态生成的导航菜单、可自定义的组织架构、可扩展的商品分类树。

  • 聚合功能需求场景:需对树形结构的“整体”进行聚合计算或操作,如计算文件夹总大小、统计部门总人数、汇总商品分类的总销量,通过组合节点的递归逻辑可快速实现。

典型实战案例

  • 办公软件的“形状组合”:单个图形(矩形、圆形)为叶子节点,组合图形为组合节点,客户端可统一操作单个图形或组合图形(移动、缩放、删除),无需区分类型。

  • 电商系统的“商品分类树”:一级分类、二级分类为组合节点,具体商品为叶子节点,客户端可统一遍历所有分类和商品,或计算某一分类下的商品总数、总销量。

  • 权限系统的“角色树”:父角色(如管理员)为组合节点,子角色(如普通管理员)、具体权限(如查看、编辑)为叶子节点,可统一校验角色的所有权限,或批量分配权限。

  • 日志系统的“日志层级”:单个日志项为叶子节点,日志组(如按模块划分的日志)为组合节点,可统一查询某一组日志的所有内容,或统计日志总数。

五、总结

组合模式的核心是“统一单个对象与组合对象的访问接口”,通过树形结构封装“部分-整体”的层次关系,借助递归逻辑实现组合节点的功能聚合,让客户端无需关注结构的复杂性,只需通过统一接口操作所有节点,大幅简化代码逻辑,提升系统的可扩展性。

从多语言实现来看,尽管各语言的语法特性差异显著,但核心逻辑高度统一,且均能适配自身的设计理念:

  • 面向对象语言(C#、Python、C++):通过抽象类/接口+多态特性,天然实现组合模式的统一访问,代码结构清晰、优雅,适配大多数企业级开发场景,无需手动模拟接口和多态。

  • Go语言:遵循“组合优于继承”,通过接口定义统一规范,结构体组合实现节点功能,无需继承,代码极简、高效,贴合语言的设计哲学,同时完美支撑树形结构的管理。

  • 纯C语言:通过结构体+函数指针+类型标记,手动模拟面向对象的接口和多态,虽代码冗余、内存管理复杂,但能实现组合模式的核心逻辑,适配嵌入式、底层开发等资源受限场景。

在工程实践中,使用组合模式需把握三个核心原则:一是明确业务场景是否存在“部分-整体”的树形层次关系,避免在无层级结构的场景中滥用;二是平衡接口的统一性与纯度,避免接口包含过多冗余操作,减少叶子节点的空实现或异常抛出;三是控制树形结构的深度,避免深层递归带来的性能损耗和栈溢出风险。

总体而言,当业务场景具备明显的层级结构、且需要客户端统一操作单个对象与组合对象时,组合模式是最优设计方案之一,它能让层次化业务的代码更清晰、易维护、可扩展,是每一位开发者必备的架构设计工具。