欢迎光临
我们一直在努力

09、Python组合模式

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

Python组合模式封面

文章目录

  • Python组合模式:从树形结构到企业级应用,一篇讲透
    • 一、痛点场景:当你的代码被树形结构逼疯
    • 二、痛点的解决方案:组合模式登场
    • 三、组合模式是什么
      • 3.1 专业解释
      • 3.2 大白话解释
      • 3.3 生活案例
    • 四、为什么要用组合模式
      • 4.1 解决的核心问题
      • 4.2 优点
      • 4.3 缺点
    • 五、组合模式是怎么演进过来的
      • 5.1 1994年:GoF正式定义
      • 5.2 2000年代:GUI框架的黄金时代
      • 5.3 2010年代:Web与前端框架的核心思想
      • 5.4 2020年代:云原生与企业级系统
    • 六、组合模式怎么用:两种实现方式
      • 6.1 透明组合模式
      • 6.2 安全组合模式
      • 6.3 如何选择
    • 七、常用场景教学:文件系统实战
    • 八、企业项目中如何使用
      • 8.1 场景一:企业组织架构与权限继承
      • 8.2 场景二:电商商品分类与指标汇总
    • 九、面试官高频面试题
      • 9.1 组合模式的定义和核心思想是什么?
      • 9.2 透明组合模式和安全组合模式的区别?
      • 9.3 组合模式和装饰器模式有什么区别?
      • 9.4 组合模式适用于什么场景?
      • 9.5 组合模式有什么缺点?如何规避?
      • 9.6 组合模式中的递归操作如何避免无限循环?
      • 9.7 组合模式和迭代器模式如何配合使用?
    • 十、总结

一、痛点场景:当你的代码被树形结构逼疯

想象一下,你正在开发一个企业级的文件管理系统。需求很简单:统计一个文件夹下所有文件的总大小。

你一开始写得很顺利:

def calculate_folder_size(folder):
total = 0
for file in folder.files:
total += file.size
return total

但很快,产品经理告诉你:文件夹里还可以有子文件夹,子文件夹里还可以有子子文件夹,嵌套层级不限。

你改了一版:

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

看起来没问题。但三天后,新需求又来了:需要支持"快捷方式",快捷方式可以指向文件也可以指向文件夹。然后是"压缩包",压缩包解压后里面又是一套文件和文件夹。再然后是"云盘挂载目录",它的行为和本地文件夹不一样……

你的代码开始变成这样:

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

这些场景的共同特征是:单个对象和组合对象在客户端看来应该被一致对待,但传统写法迫使你不断区分"这是叶子还是容器"。

二、痛点的解决方案:组合模式登场

组合模式的核心思想只有一句话:让叶子对象和组合对象实现同一个接口,客户端用统一的方式处理它们,不需要区分谁是谁。

回到文件系统的例子,用组合模式重构后:

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)

客户端调用时:

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种经典设计模式之一,属于结构型设计模式。

官方定义:将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

组合模式包含三个核心角色:

组合模式核心角色UML图

  • 抽象构件(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)定义在哪里"。

    透明模式vs安全模式对比图

    6.1 透明组合模式

    透明模式将管理子节点的方法全部定义在抽象构件Component中,这样叶子和组合都有相同的接口。客户端完全不需要区分类型,"透明"地对待所有节点。

    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接口只声明公共的业务方法。这样叶子节点不会有多余的方法,类型安全,但客户端在需要管理子节点时必须区分类型。

    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上有效。

    七、常用场景教学:文件系统实战

    文件系统是组合模式最经典的应用场景。下面我们用组合模式实现一个支持文件、文件夹、快捷方式三种节点的文件系统,并实现统计大小、树形展示、搜索三个核心功能。

    文件系统树形结构图

    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系统中,组织架构是典型的树形结构。公司下面有部门,部门下面有子部门,子部门下面有员工。常见需求包括:统计部门人数、计算部门薪资总额、权限按部门递归继承。

    企业组织架构图

    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 场景二:电商商品分类与指标汇总

    电商平台的商品分类是另一个典型的组合模式应用。一级分类包含二级分类,二级分类包含三级分类,三级分类下是具体商品。运营需要按分类维度统计商品数、销售额、库存等指标。

    电商商品分类树图

    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方法直接返回,不再递归。
  • 使用访问者模式或迭代器模式:对于复杂的遍历需求,可以结合访问者模式封装遍历逻辑,或用迭代器模式控制遍历过程。
  • 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 判断中解脱出来,变得清晰、可扩展、易维护。


    转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

    赞(0)
    未经允许不得转载:171主机测评 » 09、Python组合模式
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址