Python组合模式:从树形结构到企业级应用,一篇讲透

文章目录
- Python组合模式:从树形结构到企业级应用,一篇讲透
-
- 一、痛点场景:当你的代码被树形结构逼疯
- 二、痛点的解决方案:组合模式登场
- 三、组合模式是什么
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话解释](#3.2 大白话解释)
- [3.3 生活案例](#3.3 生活案例)
- 四、为什么要用组合模式
-
- [4.1 解决的核心问题](#4.1 解决的核心问题)
- [4.2 优点](#4.2 优点)
- [4.3 缺点](#4.3 缺点)
- 五、组合模式是怎么演进过来的
-
- [5.1 1994年:GoF正式定义](#5.1 1994年:GoF正式定义)
- [5.2 2000年代:GUI框架的黄金时代](#5.2 2000年代:GUI框架的黄金时代)
- [5.3 2010年代:Web与前端框架的核心思想](#5.3 2010年代:Web与前端框架的核心思想)
- [5.4 2020年代:云原生与企业级系统](#5.4 2020年代:云原生与企业级系统)
- 六、组合模式怎么用:两种实现方式
-
- [6.1 透明组合模式](#6.1 透明组合模式)
- [6.2 安全组合模式](#6.2 安全组合模式)
- [6.3 如何选择](#6.3 如何选择)
- 七、常用场景教学:文件系统实战
- 八、企业项目中如何使用
-
- [8.1 场景一:企业组织架构与权限继承](#8.1 场景一:企业组织架构与权限继承)
- [8.2 场景二:电商商品分类与指标汇总](#8.2 场景二:电商商品分类与指标汇总)
- 九、面试官高频面试题
-
- [9.1 组合模式的定义和核心思想是什么?](#9.1 组合模式的定义和核心思想是什么?)
- [9.2 透明组合模式和安全组合模式的区别?](#9.2 透明组合模式和安全组合模式的区别?)
- [9.3 组合模式和装饰器模式有什么区别?](#9.3 组合模式和装饰器模式有什么区别?)
- [9.4 组合模式适用于什么场景?](#9.4 组合模式适用于什么场景?)
- [9.5 组合模式有什么缺点?如何规避?](#9.5 组合模式有什么缺点?如何规避?)
- [9.6 组合模式中的递归操作如何避免无限循环?](#9.6 组合模式中的递归操作如何避免无限循环?)
- [9.7 组合模式和迭代器模式如何配合使用?](#9.7 组合模式和迭代器模式如何配合使用?)
- 十、总结
一、痛点场景:当你的代码被树形结构逼疯
想象一下,你正在开发一个企业级的文件管理系统。需求很简单:统计一个文件夹下所有文件的总大小。
你一开始写得很顺利:
python
def calculate_folder_size(folder):
total = 0
for file in folder.files:
total += file.size
return total
但很快,产品经理告诉你:文件夹里还可以有子文件夹,子文件夹里还可以有子子文件夹,嵌套层级不限。
你改了一版:
python
def calculate_folder_size(folder):
total = 0
for file in folder.files:
total += file.size
for sub_folder in folder.sub_folders:
total += calculate_folder_size(sub_folder)
return total
看起来没问题。但三天后,新需求又来了:需要支持"快捷方式",快捷方式可以指向文件也可以指向文件夹。然后是"压缩包",压缩包解压后里面又是一套文件和文件夹。再然后是"云盘挂载目录",它的行为和本地文件夹不一样......
你的代码开始变成这样:
python
def calculate_size(node):
if isinstance(node, File):
return node.size
elif isinstance(node, Folder):
total = 0
for child in node.children:
total += calculate_size(child)
return total
elif isinstance(node, Shortcut):
return calculate_size(node.target)
elif isinstance(node, ZipArchive):
return calculate_size(node.extracted_root)
elif isinstance(node, CloudMount):
return node.fetch_remote_size()
else:
raise TypeError(f"未知类型: {type(node)}")
每加一种节点类型,你就要在所有操作函数里加一个 elif 分支。统计大小要改、删除要改、复制要改、搜索要改、权限检查要改。代码里到处都是 isinstance 判断,维护成本指数级上升。
这就是典型的"部分-整体"层次结构带来的痛点。类似的场景无处不在:
- 公司组织架构:CEO管部门,部门管小组,小组管员工
- 电商商品分类:一级分类、二级分类、三级分类、具体商品
- GUI界面:窗口包含面板,面板包含按钮,按钮是最小组件
- 菜单系统:一级菜单、二级菜单、菜单项
- 前端DOM树:html包含body,body包含div,div包含span
这些场景的共同特征是:单个对象和组合对象在客户端看来应该被一致对待,但传统写法迫使你不断区分"这是叶子还是容器"。
二、痛点的解决方案:组合模式登场
组合模式的核心思想只有一句话:让叶子对象和组合对象实现同一个接口,客户端用统一的方式处理它们,不需要区分谁是谁。
回到文件系统的例子,用组合模式重构后:
python
from abc import ABC, abstractmethod
class FileSystemNode(ABC):
"""抽象构件:文件和文件夹的统一接口"""
def __init__(self, name):
self.name = name
@abstractmethod
def get_size(self):
"""计算大小,文件返回自身大小,文件夹递归计算子节点"""
pass
class File(FileSystemNode):
"""叶子构件:文件,没有子节点"""
def __init__(self, name, size):
super().__init__(name)
self.size = size
def get_size(self):
return self.size
class Folder(FileSystemNode):
"""组合构件:文件夹,可以包含文件和子文件夹"""
def __init__(self, name):
super().__init__(name)
self.children = []
def add(self, node):
self.children.append(node)
def remove(self, node):
self.children.remove(node)
def get_size(self):
return sum(child.get_size() for child in self.children)
客户端调用时:
python
root = Folder("root")
docs = Folder("docs")
docs.add(File("resume.pdf", 1024))
docs.add(File("report.docx", 2048))
root.add(docs)
root.add(File("readme.txt", 512))
print(root.get_size()) # 输出: 3584,自动递归计算
注意看客户端代码:root.get_size() 和 File("readme.txt", 512).get_size() 调用方式完全一样。客户端根本不需要知道当前节点是文件还是文件夹,它只需要调用 get_size() 就行。
如果以后要加"快捷方式"或"压缩包",只需要让它们也继承 FileSystemNode 并实现 get_size(),所有现有的客户端代码一行都不用改。这就是组合模式的威力。
三、组合模式是什么
3.1 专业解释
组合模式(Composite Pattern),又称部分-整体模式(Part-Whole Pattern),是GoF(Gang of Four,四人帮)定义的23种经典设计模式之一,属于结构型设计模式。
官方定义:将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
组合模式包含三个核心角色:

- 抽象构件(Component):为叶子构件和组合构件声明公共接口,可以包含所有子类共有行为的声明和实现。在透明模式中,Component还会声明管理子对象的方法(add、remove、getChild)。
- 叶子构件(Leaf):表示叶子节点对象,没有子节点。它实现了Component中定义的公共接口。对于管理子对象的方法(如果有),叶子节点通常抛出异常或空实现。
- 组合构件(Composite):表示容器节点对象,包含子节点。子节点可以是叶子,也可以是另一个组合构件。它实现了Component中定义的公共接口,通常以递归方式调用子节点的对应方法。
3.2 大白话解释
说人话就是:把"单个的东西"和"装东西的盒子"当成同一种东西来对待。
你去超市买东西,一瓶可乐是"商品",一箱可乐也是"商品"。收银员扫一瓶可乐收一次钱,扫一箱可乐也收一次钱(箱内价格已打包)。你不需要告诉收银员"这是一瓶所以按瓶算,这是一箱所以按箱算",它们在收银台面前是等价的。
组合模式就是让"一瓶可乐"(叶子)和"一箱可乐"(组合)都实现同一个"可结算"接口,收银员(客户端)统一调用"结算"方法,至于里面是一瓶还是一箱,由对象自己内部处理。
3.3 生活案例
军队编制是最经典的组合模式生活案例。
- 士兵(Leaf):最基本的战斗单位,不能再分
- 班(Composite):由10个士兵组成
- 排(Composite):由3个班组成
- 连(Composite):由3个排组成
- 营(Composite):由3个连组成
当司令下达"进攻"命令时,他不需要区分面前是一个士兵还是一个营。他只需要对当前单位喊"进攻"。士兵听到后自己冲上去,营长听到后逐级传达给下属,最终所有士兵都冲上去。
这就是组合模式:上级对下级的操作是统一的,具体执行由各层级自己递归分发。
四、为什么要用组合模式
4.1 解决的核心问题
组合模式解决的是**"部分-整体"层次结构中,客户端代码需要不断区分叶子节点和容器节点**的问题。
不用组合模式时,客户端代码充满了 isinstance 判断和类型分支,每加一种节点类型就要修改所有操作函数,违反开闭原则。
用了组合模式后,客户端面向抽象构件编程,完全不需要知道具体类型,新增节点类型时客户端代码零修改。
4.2 优点
- 客户端调用简单:客户端可以一致地使用组合对象和单个对象,不需要关心处理的是叶子还是容器。
- 节点自由增加:在组合体内加入新的对象很容易,客户端不会因为加入了新的对象而更改源代码,满足开闭原则。
- 递归操作天然支持:树形结构的遍历、统计、搜索等操作天然适合递归,组合模式让递归逻辑封装在Composite内部,客户端无感。
- 统一的抽象层:叶子和容器共享同一个接口,便于形成统一的领域模型。
4.3 缺点
- 违反依赖倒置原则(安全模式下):在安全组合模式中,叶子和树枝的声明都是实现类而不是接口,客户端可能需要依赖具体类。
- 设计较复杂:客户端需要花更多时间理清类之间的层次关系,对于不熟悉树形结构的开发者有一定学习成本。
- 不容易限制容器中的构件:组合模式通常不对子节点的类型做限制,如果需要特定约束(如"文件夹下只能放文件不能放文件夹"),需要额外的校验逻辑。
- 透明模式下叶子有多余方法:透明模式中叶子节点继承了add、remove等管理子节点的方法,但这些方法对叶子没有意义,通常只能抛异常,违反接口隔离原则。
五、组合模式是怎么演进过来的

5.1 1994年:GoF正式定义
组合模式的正式定义出自1994年出版的《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software),由Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides四人(人称GoF,四人帮)共同撰写。这本书系统地整理了23种经典设计模式,组合模式是其中7种结构型模式之一。
在GoF的原始定义中,组合模式的动机就是解决"图形编辑器中,图元(Graphic)可以是简单的线条、矩形,也可以是由多个图元组成的组合图元"的问题。客户端应该能统一处理简单图元和组合图元。
5.2 2000年代:GUI框架的黄金时代
组合模式在GUI框架中得到了最广泛的应用。几乎所有主流GUI框架的组件体系都是组合模式的经典实现:
- Java AWT/Swing :
Component是抽象构件,Button、Label是叶子,Container、JPanel是组合。Container可以包含任意Component,包括另一个Container。 - Qt :
QObject是所有对象的基类,QWidget可以有父子关系,形成组件树。 - MFC :
CWnd体系同样是树形组合结构。 - .NET WinForms/WPF :
Control可以包含子控件,逻辑树和视觉树都是组合模式。
这个时代的开发者发现,用组合模式构建UI组件树后,"重绘"、"布局计算"、"事件分发"等操作可以统一递归处理,极大简化了GUI框架的设计。
5.3 2010年代:Web与前端框架的核心思想
Web时代,组合模式从桌面端延伸到了浏览器:
- DOM树 :HTML文档本身就是一棵组合树。
Element可以包含其他Element或Text节点,Node接口是统一的抽象构件。querySelectorAll、appendChild等操作对所有节点类型一致。 - React虚拟DOM :React的
Element树本质上就是组合模式。组件可以是原生DOM节点(叶子),也可以是自定义组件(组合,内部又渲染其他组件)。React.createElement创建的节点统一通过render递归处理。 - Vue组件树:Vue的单文件组件可以嵌套使用,形成组件树,父子组件通过props和事件通信,整体结构就是组合模式。
前端开发者每天都在使用组合模式,只是很多人没有意识到。
5.4 2020年代:云原生与企业级系统
进入云原生时代,组合模式的应用场景进一步扩展:
- Kubernetes资源树 :Namespace包含Deployment,Deployment包含ReplicaSet,ReplicaSet包含Pod。
kubectl get可以对任意层级的资源统一操作。 - 配置中心层级管理:Apollo、Nacos等配置中心支持"应用-集群-命名空间"的层级配置,上层配置可以继承和覆盖下层。
- 企业组织架构系统:OA、HR系统中的部门-员工树,权限可以按部门递归继承。
- 电商商品分类:多级分类树,商品数、销售额等指标按分类递归汇总。
组合模式从最初的图形编辑器,到GUI框架,到前端DOM,再到云原生资源管理,三十年来始终是处理"部分-整体"层次结构的首选方案。它的核心思想从未改变,只是应用领域不断扩展。
六、组合模式怎么用:两种实现方式
组合模式有两种经典实现方式:透明组合模式 和安全组合模式。它们的区别在于"管理子节点的方法(add、remove、getChild)定义在哪里"。

6.1 透明组合模式
透明模式将管理子节点的方法全部定义在抽象构件Component中,这样叶子和组合都有相同的接口。客户端完全不需要区分类型,"透明"地对待所有节点。
python
from abc import ABC, abstractmethod
class Component(ABC):
"""抽象构件:声明所有公共方法,包括管理子节点的方法"""
def __init__(self, name):
self.name = name
@abstractmethod
def operation(self):
"""业务方法"""
pass
def add(self, component):
"""添加子节点,叶子节点默认不支持"""
raise NotImplementedError(f"{self.name} 不支持添加子节点")
def remove(self, component):
"""移除子节点,叶子节点默认不支持"""
raise NotImplementedError(f"{self.name} 不支持移除子节点")
def get_child(self, index):
"""获取子节点,叶子节点默认不支持"""
raise NotImplementedError(f"{self.name} 不支持获取子节点")
class Leaf(Component):
"""叶子构件:没有子节点"""
def operation(self):
print(f"叶子节点 {self.name} 执行业务操作")
class Composite(Component):
"""组合构件:包含子节点"""
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_child(self, index):
return self.children[index]
def operation(self):
print(f"组合节点 {self.name} 开始执行业务操作")
for child in self.children:
child.operation()
print(f"组合节点 {self.name} 结束执行业务操作")
# 客户端使用
if __name__ == "__main__":
root = Composite("根节点")
leaf1 = Leaf("叶子1")
leaf2 = Leaf("叶子2")
sub = Composite("子组合")
root.add(leaf1)
root.add(sub)
sub.add(leaf2)
# 客户端统一调用operation,完全不需要区分类型
root.operation()
# 透明模式下,客户端甚至可以对叶子调用add(但会抛异常)
try:
leaf1.add(leaf2)
except NotImplementedError as e:
print(f"捕获异常: {e}")
透明模式的优点:客户端完全不需要区分叶子和组合,接口完全一致,使用最简单。
透明模式的缺点:叶子节点继承了add、remove等对它无意义的方法,违反了接口隔离原则。如果客户端误对叶子调用这些方法,只能在运行时抛异常,不够安全。
6.2 安全组合模式
安全模式将管理子节点的方法只定义在Composite中,Component接口只声明公共的业务方法。这样叶子节点不会有多余的方法,类型安全,但客户端在需要管理子节点时必须区分类型。
python
from abc import ABC, abstractmethod
class Component(ABC):
"""抽象构件:只声明公共业务方法,不包含管理子节点的方法"""
def __init__(self, name):
self.name = name
@abstractmethod
def operation(self):
"""业务方法"""
pass
class Leaf(Component):
"""叶子构件:只有业务方法,没有管理子节点的方法"""
def operation(self):
print(f"叶子节点 {self.name} 执行业务操作")
class Composite(Component):
"""组合构件:包含子节点管理方法"""
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_child(self, index):
return self.children[index]
def operation(self):
print(f"组合节点 {self.name} 开始执行业务操作")
for child in self.children:
child.operation()
print(f"组合节点 {self.name} 结束执行业务操作")
# 客户端使用
if __name__ == "__main__":
root = Composite("根节点")
leaf1 = Leaf("叶子1")
leaf2 = Leaf("叶子2")
sub = Composite("子组合")
# 管理子节点的方法只能在Composite上调用,编译期/类型检查就能发现错误
root.add(leaf1)
root.add(sub)
sub.add(leaf2)
# 业务操作统一调用
root.operation()
# 安全模式下,leaf1没有add方法,IDE会直接标红,不会等到运行时才报错
# leaf1.add(leaf2) # 这行在IDE中就会报错,因为Leaf没有add方法
安全模式的优点:叶子节点不会有多余的方法,接口职责清晰,符合接口隔离原则。对叶子调用add等方法在编译期/类型检查阶段就能发现错误,更安全。
安全模式的缺点 :客户端在需要管理子节点时必须知道当前对象是Composite,不够透明。如果客户端需要动态构建树,可能需要 isinstance 判断。
6.3 如何选择
| 对比维度 | 透明组合模式 | 安全组合模式 |
|---|---|---|
| 管理子节点方法位置 | Component接口 | Composite类 |
| 客户端是否需要区分类型 | 不需要 | 需要(管理子节点时) |
| 接口隔离原则 | 违反(叶子有多余方法) | 符合 |
| 类型安全 | 运行时才能发现错误 | 编译期/类型检查发现错误 |
| 客户端复杂度 | 低 | 稍高 |
| 适用场景 | 以遍历/操作为主,很少动态增删节点 | 需要频繁动态增删节点,重视类型安全 |
实际开发建议 :Python是动态类型语言,没有编译期类型检查,安全模式的"编译期发现错误"优势不明显。因此Python项目中透明模式使用更广泛,因为它更简洁,客户端更简单。但可以通过类型注解(typing)和文档明确告知哪些方法只在Composite上有效。
七、常用场景教学:文件系统实战
文件系统是组合模式最经典的应用场景。下面我们用组合模式实现一个支持文件、文件夹、快捷方式三种节点的文件系统,并实现统计大小、树形展示、搜索三个核心功能。

python
from abc import ABC, abstractmethod
from typing import List, Optional
class FileSystemNode(ABC):
"""抽象构件:文件系统节点的统一接口"""
def __init__(self, name: str):
self.name = name
self.parent: Optional["Folder"] = None
@abstractmethod
def get_size(self) -> int:
"""计算节点大小(字节)"""
pass
@abstractmethod
def display(self, indent: int = 0) -> None:
"""树形展示节点结构"""
pass
@abstractmethod
def search(self, keyword: str) -> List["FileSystemNode"]:
"""按名称关键词搜索节点"""
pass
def get_path(self) -> str:
"""获取节点的完整路径"""
if self.parent is None:
return self.name
return f"{self.parent.get_path()}/{self.name}"
class File(FileSystemNode):
"""叶子构件:文件"""
def __init__(self, name: str, size: int, file_type: str = "txt"):
super().__init__(name)
self.size = size
self.file_type = file_type
def get_size(self) -> int:
return self.size
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[文件] {self.name} ({self.size} bytes)")
def search(self, keyword: str) -> List[FileSystemNode]:
if keyword.lower() in self.name.lower():
return [self]
return []
class Folder(FileSystemNode):
"""组合构件:文件夹,可以包含文件和子文件夹"""
def __init__(self, name: str):
super().__init__(name)
self.children: List[FileSystemNode] = []
def add(self, node: FileSystemNode) -> None:
node.parent = self
self.children.append(node)
def remove(self, node: FileSystemNode) -> None:
if node in self.children:
node.parent = None
self.children.remove(node)
def get_size(self) -> int:
return sum(child.get_size() for child in self.children)
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[文件夹] {self.name} ({self.get_size()} bytes)")
for child in self.children:
child.display(indent + 1)
def search(self, keyword: str) -> List[FileSystemNode]:
results = []
if keyword.lower() in self.name.lower():
results.append(self)
for child in self.children:
results.extend(child.search(keyword))
return results
class Shortcut(FileSystemNode):
"""叶子构件的变种:快捷方式,指向另一个节点"""
def __init__(self, name: str, target: FileSystemNode):
super().__init__(name)
self.target = target
def get_size(self) -> int:
# 快捷方式本身很小,这里简化为返回目标大小
return self.target.get_size()
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[快捷方式] {self.name} -> {self.target.get_path()}")
def search(self, keyword: str) -> List[FileSystemNode]:
if keyword.lower() in self.name.lower():
return [self]
return []
# 客户端使用
if __name__ == "__main__":
# 构建文件系统树
root = Folder("root")
docs = Folder("Documents")
pics = Folder("Pictures")
projects = Folder("Projects")
docs.add(File("resume.pdf", 102400, "pdf"))
docs.add(File("report.docx", 204800, "docx"))
docs.add(File("notes.txt", 5120, "txt"))
pics.add(File("vacation.jpg", 5120000, "jpg"))
pics.add(File("avatar.png", 204800, "png"))
projects.add(File("main.py", 3072, "py"))
projects.add(File("README.md", 1024, "md"))
root.add(docs)
root.add(pics)
root.add(projects)
root.add(File("readme.txt", 2048, "txt"))
# 创建快捷方式
shortcut = Shortcut("简历快捷方式", docs.children[0])
root.add(shortcut)
# 1. 统一调用get_size,客户端完全不需要区分类型
print(f"根目录总大小: {root.get_size()} bytes")
print(f"Documents大小: {docs.get_size()} bytes")
print(f"单个文件大小: {pics.children[0].get_size()} bytes")
print("\n" + "=" * 50 + "\n")
# 2. 统一调用display,递归展示整棵树
root.display()
print("\n" + "=" * 50 + "\n")
# 3. 统一调用search,递归搜索
results = root.search("readme")
print(f"搜索 'readme' 找到 {len(results)} 个结果:")
for r in results:
print(f" - {r.get_path()}")
运行输出:
根目录总大小: 5650432 bytes
Documents大小: 312320 bytes
单个文件大小: 5120000 bytes
==================================================
[文件夹] root (5650432 bytes)
[文件夹] Documents (312320 bytes)
[文件] resume.pdf (102400 bytes)
[文件] report.docx (204800 bytes)
[文件] notes.txt (5120 bytes)
[文件夹] Pictures (5324800 bytes)
[文件] vacation.jpg (5120000 bytes)
[文件] avatar.png (204800 bytes)
[文件夹] Projects (4096 bytes)
[文件] main.py (3072 bytes)
[文件] README.md (1024 bytes)
[文件] readme.txt (2048 bytes)
[快捷方式] 简历快捷方式 -> root/Documents/resume.pdf
==================================================
搜索 'readme' 找到 2 个结果:
- root/Projects/README.md
- root/readme.txt
这个例子展示了组合模式的核心价值:get_size()、display()、search() 三个方法对文件、文件夹、快捷方式的调用方式完全一致,客户端不需要任何 isinstance 判断。如果以后要加"压缩包"、"云盘目录"等新节点类型,只需继承 FileSystemNode 并实现三个抽象方法,现有代码零修改。
八、企业项目中如何使用
8.1 场景一:企业组织架构与权限继承
在企业OA和HR系统中,组织架构是典型的树形结构。公司下面有部门,部门下面有子部门,子部门下面有员工。常见需求包括:统计部门人数、计算部门薪资总额、权限按部门递归继承。

python
from abc import ABC, abstractmethod
from typing import List, Set, Optional
class OrgNode(ABC):
"""抽象构件:组织节点统一接口"""
def __init__(self, name: str, node_id: str):
self.name = name
self.node_id = node_id
self.parent: Optional["Department"] = None
self.permissions: Set[str] = set()
@abstractmethod
def count_employees(self) -> int:
"""统计员工数"""
pass
@abstractmethod
def calculate_salary(self) -> float:
"""计算薪资总额"""
pass
@abstractmethod
def get_all_permissions(self) -> Set[str]:
"""获取所有权限(含继承自上级的)"""
pass
@abstractmethod
def display(self, indent: int = 0) -> None:
"""展示组织架构"""
pass
class Employee(OrgNode):
"""叶子构件:员工"""
def __init__(self, name: str, node_id: str, salary: float, title: str):
super().__init__(name, node_id)
self.salary = salary
self.title = title
def count_employees(self) -> int:
return 1
def calculate_salary(self) -> float:
return self.salary
def get_all_permissions(self) -> Set[str]:
# 员工权限 = 自身权限 + 继承自上级部门的权限
perms = set(self.permissions)
if self.parent:
perms.update(self.parent.get_all_permissions())
return perms
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[员工] {self.name} ({self.title}, 薪资:{self.salary})")
class Department(OrgNode):
"""组合构件:部门"""
def __init__(self, name: str, node_id: str, manager: str = ""):
super().__init__(name, node_id)
self.manager = manager
self.children: List[OrgNode] = []
def add(self, node: OrgNode) -> None:
node.parent = self
self.children.append(node)
def remove(self, node: OrgNode) -> None:
if node in self.children:
node.parent = None
self.children.remove(node)
def count_employees(self) -> int:
return sum(child.count_employees() for child in self.children)
def calculate_salary(self) -> float:
return sum(child.calculate_salary() for child in self.children)
def get_all_permissions(self) -> Set[str]:
# 部门权限 = 自身权限 + 继承自上级部门的权限
perms = set(self.permissions)
if self.parent:
perms.update(self.parent.get_all_permissions())
return perms
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[部门] {self.name} (主管:{self.manager}, 人数:{self.count_employees()}, 薪资总额:{self.calculate_salary()})")
for child in self.children:
child.display(indent + 1)
# 企业项目实战
if __name__ == "__main__":
# 构建组织架构
company = Department("科技有限公司", "dept_001", "张总")
company.permissions = {"查看公司公告", "使用OA系统"}
tech_dept = Department("技术部", "dept_002", "李总监")
tech_dept.permissions = {"访问代码仓库", "部署服务器"}
frontend_group = Department("前端组", "dept_003", "王组长")
frontend_group.permissions = {"访问设计稿"}
backend_group = Department("后端组", "dept_004", "赵组长")
backend_group.permissions = {"访问数据库"}
market_dept = Department("市场部", "dept_005", "钱总监")
market_dept.permissions = {"发布营销活动"}
# 添加员工
frontend_group.add(Employee("小明", "emp_001", 15000, "前端工程师"))
frontend_group.add(Employee("小红", "emp_002", 18000, "高级前端工程师"))
backend_group.add(Employee("小刚", "emp_003", 20000, "后端工程师"))
backend_group.add(Employee("小丽", "emp_004", 25000, "架构师"))
market_dept.add(Employee("小华", "emp_005", 12000, "市场专员"))
# 组装树
tech_dept.add(frontend_group)
tech_dept.add(backend_group)
company.add(tech_dept)
company.add(market_dept)
# 1. 统一统计:公司总人数和总薪资
print(f"公司总人数: {company.count_employees()}")
print(f"公司薪资总额: {company.calculate_salary()}")
print(f"技术部人数: {tech_dept.count_employees()}")
print(f"技术部薪资总额: {tech_dept.calculate_salary()}")
print("\n" + "=" * 60 + "\n")
# 2. 统一展示组织架构
company.display()
print("\n" + "=" * 60 + "\n")
# 3. 权限继承验证:前端组员工自动继承公司、技术部、前端组的所有权限
xiaoming = frontend_group.children[0]
print(f"员工 {xiaoming.name} 的所有权限:")
for perm in sorted(xiaoming.get_all_permissions()):
print(f" - {perm}")
运行输出:
公司总人数: 5
公司薪资总额: 90000.0
技术部人数: 4
技术部薪资总额: 78000.0
============================================================
[部门] 科技有限公司 (主管:张总, 人数:5, 薪资总额:90000.0)
[部门] 技术部 (主管:李总监, 人数:4, 薪资总额:78000.0)
[部门] 前端组 (主管:王组长, 人数:2, 薪资总额:33000.0)
[员工] 小明 (前端工程师, 薪资:15000)
[员工] 小红 (高级前端工程师, 薪资:18000)
[部门] 后端组 (主管:赵组长, 人数:2, 薪资总额:45000.0)
[员工] 小刚 (后端工程师, 薪资:20000)
[员工] 小丽 (架构师, 薪资:25000)
[部门] 市场部 (主管:钱总监, 人数:1, 薪资总额:12000.0)
[员工] 小华 (市场专员, 薪资:12000)
============================================================
员工 小明 的所有权限:
- 使用OA系统
- 访问代码仓库
- 访问设计稿
- 查看公司公告
这个企业级案例展示了组合模式的三个典型企业应用:递归统计 (人数、薪资)、层级展示 (组织架构树)、权限继承(子节点自动继承父节点权限)。所有操作对部门和员工的调用方式完全一致。
8.2 场景二:电商商品分类与指标汇总
电商平台的商品分类是另一个典型的组合模式应用。一级分类包含二级分类,二级分类包含三级分类,三级分类下是具体商品。运营需要按分类维度统计商品数、销售额、库存等指标。

python
from abc import ABC, abstractmethod
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class ProductStats:
"""商品统计数据"""
product_count: int = 0
total_sales: float = 0.0
total_stock: int = 0
class CategoryNode(ABC):
"""抽象构件:分类节点统一接口"""
def __init__(self, name: str, node_id: str):
self.name = name
self.node_id = node_id
self.parent: Optional["Category"] = None
@abstractmethod
def get_stats(self) -> ProductStats:
"""获取统计数据(递归汇总)"""
pass
@abstractmethod
def display(self, indent: int = 0) -> None:
"""展示分类树"""
pass
@abstractmethod
def find_products(self, keyword: str) -> List["Product"]:
"""按关键词搜索商品"""
pass
class Product(CategoryNode):
"""叶子构件:具体商品"""
def __init__(self, name: str, node_id: str, price: float, stock: int, sales: float = 0):
super().__init__(name, node_id)
self.price = price
self.stock = stock
self.sales = sales # 销售额
def get_stats(self) -> ProductStats:
return ProductStats(
product_count=1,
total_sales=self.sales,
total_stock=self.stock
)
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[商品] {self.name} (价格:{self.price}, 库存:{self.stock}, 销售额:{self.sales})")
def find_products(self, keyword: str) -> List["Product"]:
if keyword.lower() in self.name.lower():
return [self]
return []
class Category(CategoryNode):
"""组合构件:商品分类"""
def __init__(self, name: str, node_id: str, level: int = 1):
super().__init__(name, node_id)
self.level = level
self.children: List[CategoryNode] = []
def add(self, node: CategoryNode) -> None:
node.parent = self
self.children.append(node)
def remove(self, node: CategoryNode) -> None:
if node in self.children:
node.parent = None
self.children.remove(node)
def get_stats(self) -> ProductStats:
# 递归汇总所有子节点的统计数据
total = ProductStats()
for child in self.children:
child_stats = child.get_stats()
total.product_count += child_stats.product_count
total.total_sales += child_stats.total_sales
total.total_stock += child_stats.total_stock
return total
def display(self, indent: int = 0) -> None:
stats = self.get_stats()
print(" " * indent + f"[分类 L{self.level}] {self.name} (商品数:{stats.product_count}, 销售额:{stats.total_sales}, 库存:{stats.total_stock})")
for child in self.children:
child.display(indent + 1)
def find_products(self, keyword: str) -> List[Product]:
results = []
for child in self.children:
results.extend(child.find_products(keyword))
return results
# 电商项目实战
if __name__ == "__main__":
# 构建商品分类树
root = Category("全部商品", "cat_001", level=1)
electronics = Category("电子产品", "cat_002", level=2)
phones = Category("手机", "cat_003", level=3)
computers = Category("电脑", "cat_004", level=3)
clothing = Category("服装鞋帽", "cat_005", level=2)
shoes = Category("运动鞋", "cat_006", level=3)
food = Category("食品饮料", "cat_007", level=2)
# 添加商品
phones.add(Product("iPhone 15 Pro", "p_001", 8999, 500, 4500000))
phones.add(Product("华为 Mate 60 Pro", "p_002", 6999, 800, 5600000))
phones.add(Product("小米 14 Ultra", "p_003", 5999, 1200, 3600000))
computers.add(Product("MacBook Pro 14", "p_004", 14999, 200, 3000000))
computers.add(Product("ThinkPad X1 Carbon", "p_005", 9999, 300, 2000000))
shoes.add(Product("Nike Air Max", "p_006", 899, 2000, 1800000))
shoes.add(Product("Adidas Ultraboost", "p_007", 1099, 1500, 1650000))
food.add(Product("三只松鼠坚果礼盒", "p_008", 99, 5000, 500000))
# 组装分类树
electronics.add(phones)
electronics.add(computers)
clothing.add(shoes)
root.add(electronics)
root.add(clothing)
root.add(food)
# 1. 递归汇总:各层级分类的统计数据
print("=== 全平台统计 ===")
root_stats = root.get_stats()
print(f"商品总数: {root_stats.product_count}")
print(f"销售总额: {root_stats.total_sales}")
print(f"库存总量: {root_stats.total_stock}")
print("\n=== 电子产品分类统计 ===")
elec_stats = electronics.get_stats()
print(f"商品总数: {elec_stats.product_count}")
print(f"销售总额: {elec_stats.total_sales}")
print("\n=== 手机分类统计 ===")
phone_stats = phones.get_stats()
print(f"商品总数: {phone_stats.product_count}")
print(f"销售总额: {phone_stats.total_sales}")
print("\n" + "=" * 60 + "\n")
# 2. 展示完整分类树(每个节点自动显示汇总数据)
root.display()
print("\n" + "=" * 60 + "\n")
# 3. 全平台搜索
results = root.find_products("Pro")
print(f"搜索 'Pro' 找到 {len(results)} 个商品:")
for p in results:
print(f" - {p.name} (价格:{p.price})")
运行输出:
=== 全平台统计 ===
商品总数: 8
销售总额: 22650000.0
库存总量: 11500
=== 电子产品分类统计 ===
商品总数: 5
销售总额: 18700000.0
=== 手机分类统计 ===
商品总数: 3
销售总额: 13700000.0
============================================================
[分类 L1] 全部商品 (商品数:8, 销售额:22650000.0, 库存:11500)
[分类 L2] 电子产品 (商品数:5, 销售额:18700000.0, 库存:3000)
[分类 L3] 手机 (商品数:3, 销售额:13700000.0, 库存:2500)
[商品] iPhone 15 Pro (价格:8999, 库存:500, 销售额:4500000)
[商品] 华为 Mate 60 Pro (价格:6999, 库存:800, 销售额:5600000)
[商品] 小米 14 Ultra (价格:5999, 库存:1200, 销售额:3600000)
[分类 L3] 电脑 (商品数:2, 销售额:5000000.0, 库存:500)
[商品] MacBook Pro 14 (价格:14999, 库存:200, 销售额:3000000)
[商品] ThinkPad X1 Carbon (价格:9999, 库存:300, 销售额:2000000)
[分类 L2] 服装鞋帽 (商品数:2, 销售额:3450000.0, 库存:3500)
[分类 L3] 运动鞋 (商品数:2, 销售额:3450000.0, 库存:3500)
[商品] Nike Air Max (价格:899, 库存:2000, 销售额:1800000)
[商品] Adidas Ultraboost (价格:1099, 库存:1500, 销售额:1650000)
[分类 L2] 食品饮料 (商品数:1, 销售额:500000.0, 库存:5000)
[商品] 三只松鼠坚果礼盒 (价格:99, 库存:5000, 销售额:500000)
============================================================
搜索 'Pro' 找到 3 个商品:
- iPhone 15 Pro (价格:8999)
- 华为 Mate 60 Pro (价格:6999)
- MacBook Pro 14 (价格:14999)
这个电商案例展示了组合模式在企业项目中的核心价值:每个分类节点的统计数据都是通过递归自动汇总的,新增一个子分类或商品时,所有上级分类的统计数据自动更新,不需要手动维护 。运营后台展示分类树时,每个节点直接调用 get_stats() 就能拿到该分类下的汇总数据,无论这个分类下是直接挂商品还是挂了多层子分类。
九、面试官高频面试题
9.1 组合模式的定义和核心思想是什么?
参考答案:组合模式是一种结构型设计模式,将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。核心思想是让叶子对象和组合对象实现同一个抽象接口,客户端面向接口编程,不需要区分当前操作的是叶子还是容器。
9.2 透明组合模式和安全组合模式的区别?
参考答案:
- 透明模式:将管理子节点的方法(add、remove、getChild)定义在抽象构件Component中,叶子和组合有完全一致的接口。优点是客户端使用简单,不需要区分类型;缺点是叶子继承了无意义的方法,违反接口隔离原则,误调用时只能运行时抛异常。
- 安全模式:管理子节点的方法只定义在Composite中,Component只声明公共业务方法。优点是叶子没有多余方法,符合接口隔离原则,类型更安全;缺点是客户端在管理子节点时需要区分类型,不够透明。
选择建议:以遍历和统一操作为主的场景用透明模式;需要频繁动态增删节点且重视类型安全的场景用安全模式。Python动态类型语言中透明模式更常用。
9.3 组合模式和装饰器模式有什么区别?
参考答案:
- 目的不同:组合模式用于表示"部分-整体"的树形层次结构,强调对象的组合和统一处理;装饰器模式用于在不修改原有类的情况下动态扩展对象功能,强调功能的增强。
- 结构不同:组合模式中Composite包含多个子节点,形成树形结构;装饰器模式中Decorator只包含一个被装饰对象,形成链式结构。
- 客户端视角不同:组合模式中客户端统一对待叶子和组合;装饰器模式中客户端通常只关心最终装饰后的对象行为。
两者可以结合使用,例如在组合树的某个节点上用装饰器动态添加功能。
9.4 组合模式适用于什么场景?
参考答案:
- 需要表示"部分-整体"层次结构的场景,如文件系统、组织架构、商品分类、菜单系统。
- 希望客户端忽略单个对象和组合对象的差异,统一处理的场景。
- 需要对树形结构进行递归操作(统计、遍历、搜索、权限继承)的场景。
- GUI组件树、DOM树、前端组件树等天然的树形结构。
9.5 组合模式有什么缺点?如何规避?
参考答案:
- 违反依赖倒置原则:安全模式中客户端可能依赖具体类。规避:尽量面向抽象接口编程,只在必须管理子节点时才使用具体类型。
- 透明模式违反接口隔离原则:叶子有多余方法。规避:在文档和类型注解中明确标注哪些方法仅Composite有效,或使用安全模式。
- 不容易限制子节点类型:组合模式默认不限制子节点类型。规避:在Composite的add方法中添加类型校验逻辑。
- 设计较复杂:树形结构对新手不友好。规避:提供清晰的文档和示例,使用工厂方法或建造者模式辅助构建树。
9.6 组合模式中的递归操作如何避免无限循环?
参考答案:组合模式的递归操作通常通过Composite调用子节点的方法来实现。要避免无限循环,需要注意:
- 不允许循环引用:在add方法中检查目标节点是否是当前节点的祖先,如果是则拒绝添加。
- 有明确的叶子终止条件:叶子节点的operation方法直接返回,不再递归。
- 使用访问者模式或迭代器模式:对于复杂的遍历需求,可以结合访问者模式封装遍历逻辑,或用迭代器模式控制遍历过程。
python
def add(self, node):
# 防止循环引用:检查node是否是当前节点的祖先
ancestor = self
while ancestor is not None:
if ancestor is node:
raise ValueError("不允许循环引用")
ancestor = ancestor.parent
node.parent = self
self.children.append(node)
9.7 组合模式和迭代器模式如何配合使用?
参考答案:组合模式负责构建树形结构,迭代器模式负责遍历树形结构。两者配合可以将"数据结构"和"遍历算法"分离:
- 组合模式定义了树的节点结构和递归操作。
- 迭代器模式可以实现深度优先(DFS)、广度优先(BFS)等不同遍历策略。
- 客户端通过迭代器逐个访问节点,不需要关心树的内部结构和遍历顺序。
Python中可以通过实现 __iter__ 和 __next__ 方法,或使用 yield 生成器来实现组合树的迭代器。
十、总结
组合模式是处理"部分-整体"层次结构的经典方案,三十年来从GUI框架到前端DOM再到云原生管理,始终活跃在软件开发的各个领域。它的核心思想极其朴素:让单个对象和组合对象共享同一个接口,客户端用统一的方式处理它们。
掌握组合模式的关键在于理解三点:
- 三个角色:抽象构件Component定义统一接口,叶子Leaf是最小单位,组合Composite包含子节点并递归操作。
- 两种实现:透明模式接口统一但叶子有多余方法,安全模式职责清晰但客户端需区分类型。Python中推荐透明模式。
- 一个核心价值:客户端面向抽象编程,新增节点类型时现有代码零修改,完美符合开闭原则。
当你在项目中遇到树形结构、嵌套层级、递归统计这类需求时,第一反应就应该是组合模式。它能让你的代码从满屏的 isinstance 判断中解脱出来,变得清晰、可扩展、易维护。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。