09、Python组合模式

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

文章目录

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

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

你一开始写得很顺利:

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

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

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

  1. 抽象构件(Component):为叶子构件和组合构件声明公共接口,可以包含所有子类共有行为的声明和实现。在透明模式中,Component还会声明管理子对象的方法(add、remove、getChild)。
  2. 叶子构件(Leaf):表示叶子节点对象,没有子节点。它实现了Component中定义的公共接口。对于管理子对象的方法(如果有),叶子节点通常抛出异常或空实现。
  3. 组合构件(Composite):表示容器节点对象,包含子节点。子节点可以是叶子,也可以是另一个组合构件。它实现了Component中定义的公共接口,通常以递归方式调用子节点的对应方法。

3.2 大白话解释

说人话就是:把"单个的东西"和"装东西的盒子"当成同一种东西来对待。

你去超市买东西,一瓶可乐是"商品",一箱可乐也是"商品"。收银员扫一瓶可乐收一次钱,扫一箱可乐也收一次钱(箱内价格已打包)。你不需要告诉收银员"这是一瓶所以按瓶算,这是一箱所以按箱算",它们在收银台面前是等价的。

组合模式就是让"一瓶可乐"(叶子)和"一箱可乐"(组合)都实现同一个"可结算"接口,收银员(客户端)统一调用"结算"方法,至于里面是一瓶还是一箱,由对象自己内部处理。

3.3 生活案例

军队编制是最经典的组合模式生活案例。

  • 士兵(Leaf):最基本的战斗单位,不能再分
  • 班(Composite):由10个士兵组成
  • 排(Composite):由3个班组成
  • 连(Composite):由3个排组成
  • 营(Composite):由3个连组成

当司令下达"进攻"命令时,他不需要区分面前是一个士兵还是一个营。他只需要对当前单位喊"进攻"。士兵听到后自己冲上去,营长听到后逐级传达给下属,最终所有士兵都冲上去。

这就是组合模式:上级对下级的操作是统一的,具体执行由各层级自己递归分发。

四、为什么要用组合模式

4.1 解决的核心问题

组合模式解决的是**"部分-整体"层次结构中,客户端代码需要不断区分叶子节点和容器节点**的问题。

不用组合模式时,客户端代码充满了 isinstance 判断和类型分支,每加一种节点类型就要修改所有操作函数,违反开闭原则。

用了组合模式后,客户端面向抽象构件编程,完全不需要知道具体类型,新增节点类型时客户端代码零修改。

4.2 优点

  1. 客户端调用简单:客户端可以一致地使用组合对象和单个对象,不需要关心处理的是叶子还是容器。
  2. 节点自由增加:在组合体内加入新的对象很容易,客户端不会因为加入了新的对象而更改源代码,满足开闭原则。
  3. 递归操作天然支持:树形结构的遍历、统计、搜索等操作天然适合递归,组合模式让递归逻辑封装在Composite内部,客户端无感。
  4. 统一的抽象层:叶子和容器共享同一个接口,便于形成统一的领域模型。

4.3 缺点

  1. 违反依赖倒置原则(安全模式下):在安全组合模式中,叶子和树枝的声明都是实现类而不是接口,客户端可能需要依赖具体类。
  2. 设计较复杂:客户端需要花更多时间理清类之间的层次关系,对于不熟悉树形结构的开发者有一定学习成本。
  3. 不容易限制容器中的构件:组合模式通常不对子节点的类型做限制,如果需要特定约束(如"文件夹下只能放文件不能放文件夹"),需要额外的校验逻辑。
  4. 透明模式下叶子有多余方法:透明模式中叶子节点继承了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/SwingComponent是抽象构件,ButtonLabel是叶子,ContainerJPanel是组合。Container可以包含任意Component,包括另一个Container
  • QtQObject是所有对象的基类,QWidget可以有父子关系,形成组件树。
  • MFCCWnd体系同样是树形组合结构。
  • .NET WinForms/WPFControl可以包含子控件,逻辑树和视觉树都是组合模式。

这个时代的开发者发现,用组合模式构建UI组件树后,"重绘"、"布局计算"、"事件分发"等操作可以统一递归处理,极大简化了GUI框架的设计。

5.3 2010年代:Web与前端框架的核心思想

Web时代,组合模式从桌面端延伸到了浏览器:

  • DOM树 :HTML文档本身就是一棵组合树。Element可以包含其他ElementText节点,Node接口是统一的抽象构件。querySelectorAllappendChild等操作对所有节点类型一致。
  • 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 组合模式适用于什么场景?

参考答案

  1. 需要表示"部分-整体"层次结构的场景,如文件系统、组织架构、商品分类、菜单系统。
  2. 希望客户端忽略单个对象和组合对象的差异,统一处理的场景。
  3. 需要对树形结构进行递归操作(统计、遍历、搜索、权限继承)的场景。
  4. GUI组件树、DOM树、前端组件树等天然的树形结构。

9.5 组合模式有什么缺点?如何规避?

参考答案

  1. 违反依赖倒置原则:安全模式中客户端可能依赖具体类。规避:尽量面向抽象接口编程,只在必须管理子节点时才使用具体类型。
  2. 透明模式违反接口隔离原则:叶子有多余方法。规避:在文档和类型注解中明确标注哪些方法仅Composite有效,或使用安全模式。
  3. 不容易限制子节点类型:组合模式默认不限制子节点类型。规避:在Composite的add方法中添加类型校验逻辑。
  4. 设计较复杂:树形结构对新手不友好。规避:提供清晰的文档和示例,使用工厂方法或建造者模式辅助构建树。

9.6 组合模式中的递归操作如何避免无限循环?

参考答案:组合模式的递归操作通常通过Composite调用子节点的方法来实现。要避免无限循环,需要注意:

  1. 不允许循环引用:在add方法中检查目标节点是否是当前节点的祖先,如果是则拒绝添加。
  2. 有明确的叶子终止条件:叶子节点的operation方法直接返回,不再递归。
  3. 使用访问者模式或迭代器模式:对于复杂的遍历需求,可以结合访问者模式封装遍历逻辑,或用迭代器模式控制遍历过程。
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再到云原生管理,始终活跃在软件开发的各个领域。它的核心思想极其朴素:让单个对象和组合对象共享同一个接口,客户端用统一的方式处理它们

掌握组合模式的关键在于理解三点:

  1. 三个角色:抽象构件Component定义统一接口,叶子Leaf是最小单位,组合Composite包含子节点并递归操作。
  2. 两种实现:透明模式接口统一但叶子有多余方法,安全模式职责清晰但客户端需区分类型。Python中推荐透明模式。
  3. 一个核心价值:客户端面向抽象编程,新增节点类型时现有代码零修改,完美符合开闭原则。

当你在项目中遇到树形结构、嵌套层级、递归统计这类需求时,第一反应就应该是组合模式。它能让你的代码从满屏的 isinstance 判断中解脱出来,变得清晰、可扩展、易维护。


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

相关推荐
lvts_cs15 小时前
如何评估化工产业规划的质量
大数据·人工智能·动态规划
一航jason15 小时前
安卓车机端 AIOS 技术生态全景
android·人工智能·ai·ai编程·ai-native
故七月15 小时前
锦邻创享OPC社区:构建全要素创业生态,打造城市创新发展新引擎
大数据·人工智能
Mickey Q15 小时前
【深度学习】感知机基础
人工智能·深度学习
夫唯不争,故无尤也15 小时前
Agentic Search + RL :打通从RL原理到实际训练全流程
人工智能·深度学习·机器学习·强化学习·rl
H03111698515 小时前
项目汇报PPT模板平台梳理:六个渠道的特点与适用场景
人工智能
Escalating_xu15 小时前
【C++入门基础(上)】从发展历程、学习路线到命名空间与输入输出
开发语言·c++
Tenifs15 小时前
Python Loguru 使用指南
python·loguru