系统建模需要精确性。当架构师和开发人员绘制复杂的软件结构时,组件之间的关系决定了系统的行为、可扩展性以及应对变化的能力。在复合结构图中,两种特定的关系类型常常引起混淆:聚合与组合。尽管它们都表示整体-部分关系,但两者之间的区别决定了所有权、生命周期管理以及依赖强度。
理解这些细微差别并不仅仅是学术上的需求。它会影响内存管理方式、数据持久化机制,以及不同子系统之间的耦合程度。本指南深入探讨这些结构概念,超越基本定义,深入分析它们在系统设计中的实际影响。

🏗️ 基础:复合结构图
复合结构图展示了分类器的内部结构。它显示了分类器如何被划分为嵌套的组件,以及这些组件如何通过端口和连接器相互交互。在这个内部结构中,部件与整体的连接方式具有重要意义。
想象一个复杂的装配体。你有一个中心单元,将较小的单元连接到它上面。有时,当中心单元被破坏时,较小的单元仍然存在;而有时,当中心单元被破坏时,较小的单元也随之消失。这种区别正是聚合与组合之间核心差异所在。
- 复合结构图 关注内部架构。
- 整体-部分关系 定义这些内部组件之间的连接方式。
- 所有权 决定谁对部件的生命周期负责。
🤝 聚合:弱整体-部分关系
聚合表示一种关系,其中一个对象(整体)包含或引用另一个对象(部分),但该部分可以独立存在。它通常被描述为一种“共享”或“弱”关系。在这种情况下,部分的生命周期并不严格依赖于整体的生命周期。
🔍 聚合的关键特征
- 独立性: 部分可以在没有整体的情况下存在。
- 共享所有权: 部分可能同时属于多个整体。
- 弱耦合: 对整体的更改不一定会影响部分的存在。
- 方向性: 通常以一条线加一个空心菱形(位于整体端)来表示。
考虑一个涉及大学及其院系的情景。院系存在于大学结构中。然而,如果大学关闭了某栋建筑,院系对象本身可能仍会保留在数据库或内存中用于归档目的,或者被重新分配到另一个行政单位。更准确地说,考虑一个团队及其队员。如果一个团队解散,队员作为个体仍然存在。他们可以加入另一个团队。从严格的生命周期角度看,队员并非被团队独占拥有。
🧩 实现上的影响
在建模聚合时,你承认存在依赖关系,但并非创建依赖关系。管理‘整体’的代码或逻辑无需实例化‘部分’。部分可以通过注入、作为参数传递,或从共享池中获取。这降低了初始化逻辑的复杂性。
实现方面的关键点:
- 无构造函数依赖: 你无需在整体的构造函数中创建部分。
- 引用传递 整体持有对部分的引用(指针或ID)。
- 垃圾回收: 销毁整体不会自动触发部分的销毁。
💥 组合:强部分-整体关系
组合代表了一种更强的聚合形式。它意味着独占所有权。部分是整体的不可或缺的组成部分,其生命周期与整体的生命周期严格绑定。如果整体被销毁,部分也会随之被销毁。
🔍 组合的关键特征
- 依赖性: 部分不能脱离整体而存在。
- 独占所有权: 一个部分在同一时间只能属于一个整体。
- 强耦合: 整体的创建和销毁决定了部分的创建和销毁。
- 方向性: 表示为一条线,整体一端带有实心菱形。
想象一栋房子及其房间。房间的存在依赖于房子的存在。如果房子被拆除,房间在该上下文中就不再作为功能性实体存在。你无法将一个房间从一栋房子移到另一栋房子而不从根本上改变其身份。同样地,考虑一辆汽车及其发动机。虽然发动机可以拆下维修,但在汽车存在的背景下,特定的发动机实例是不可或缺的。如果汽车被报废,那个特定的发动机配置也就实际上消失了。
🧩 实现上的影响
在建模组合时,整体负责部分的存在。这通常意味着在整体内部进行实例化。
- 构造函数依赖: 整体通常在初始化期间创建部分。
- 资源管理: 整体必须确保在整体被销毁时,分配给部分的资源被释放。
- 生命周期同步: 部分不能在多个整体之间共享。
⚖️ 聚合与组合:详细对比
为了澄清这些概念之间的区别,我们可以将它们并列比较。下表分解了与系统架构和绘图相关的操作差异。
| 特性 | 聚合 | 组合 |
|---|---|---|
| 所有权 | 共享或弱 | 独占的 |
| 生命周期 | 独立的 | 依赖的 |
| 创建 | 整体外部 | 整体内部 |
| 销毁 | 整体消亡 → 部分存活 | 整体消亡 → 部分消亡 |
| 关联 | 可实现多向关联 | 严格单向所有权 |
| 符号 | 空心菱形 (◇) | 实心菱形 (◆) |
| 类比 | 团队与队员 | 房屋与房间 |
🛠️ 复合结构图中的视觉符号
在复合结构图中,这些关系通过分类器内部部件之间的特定连接器来可视化。这种符号有助于开发人员和架构师无需阅读代码即可快速理解结构约束。
- 连接器: 一条连接容器部分与被包含部分的直线。
- 菱形(聚合): 容器一侧的空心菱形表示聚合。它表明这种关系是“拥有-有”关系,但没有严格的拥有权。
- 菱形(组合): 容器一侧的实心菱形表示组合。它表明这是一种具有严格所有权的“部分-整体”关系。
尽管视觉符号是标准的,但其解释取决于设计阶段赋予的语义含义。实心菱形意味着一种契约:‘我负责这个部分的生命。’
🔄 生命周期管理与所有权规则
这些关系中最关键的方面之一是它们如何影响对象的生命周期。这在内存管理、数据库事务和资源释放中尤为重要。
🗑️ 破坏场景
当容器对象从内存或系统中移除时:
- 组合场景: 系统会递归地销毁所有组成的部分。如果你有一个包含页面的文档,删除文档的同时也会删除所有页面。系统不会尝试将页面保存到其他地方。
- 聚合场景: 系统会移除对部分的引用。该部分仍保留在系统状态中。系统必须确保该部分不会以破坏数据完整性的形式被遗弃,但该部分本身不会被销毁。
🔁 重新分配的可能性
组合禁止重新分配。一个部分无法在不被重新创建或重构的情况下从一个整体转移到另一个整体。聚合允许重新分配。一个资源(如打印机)可以被多个计算机聚合。如果计算机A关闭,打印机仍可供计算机B使用。
🌍 结构建模的实际场景
为了使这些概念更具实际意义,让我们考察企业系统中常见的抽象场景。
场景A:订单处理系统
在订单管理系统中,一个订单包含订单项.
- 关系: 组合。
- 理由: 订单项通常在没有订单的情况下没有意义。在这个特定模型中,你通常不会独立于订单上下文单独销售某个项目。如果订单被取消(销毁),与之关联的订单项将从活动上下文中删除。
场景B:员工目录
一个部门包含员工.
- 关系: 聚合。
- 理由: 员工独立于部门存在。他们可能处于休假、调动或被解雇状态。如果部门进行重组,员工对象仍然存在。这种关系是一种集合关系,而非所有权关系。
情景C:金融投资组合
一个投资组合持有股票.
- 关系: 聚合。
- 理由: 一只股票在市场中存在,无论哪个投资组合持有它。一个单一的股票实例可能被多个投资组合对象引用。销毁一个投资组合并不会销毁股票数据。
🚧 常见陷阱与误解
设计师经常混淆这两个概念,导致本应松散耦合的地方变成了紧密耦合,反之亦然。以下是一些需要避免的常见错误。
- 假设组合意味着数据持久化: 组合在模型中定义了生命周期关系。除非底层实现强制执行,否则它不能保证数据库的级联删除。然而,模型应反映设计意图。
- 使用组合来处理共享资源: 如果两个组件需要共享一个资源的单一实例(如数据库连接池),则使用组合是错误的。应使用聚合。组合会阻止共享。
- 忽略“部分”的定义: 在复合结构图中,“部分”是一个特定实例。如果你在建模类本身,你实际上是在建模类关联。请确保你能够区分类定义与实例关系。
- 过度使用组合: 组合会产生强依赖关系。这会使重构变得困难。如果你将一个模块组合到主应用程序中,而你需要更换该模块,就必须重新构建主应用程序的结构。聚合则提供了更高的灵活性。
📈 对系统设计与维护的影响
在聚合与组合之间进行选择会影响软件的长期可维护性。它会影响团队与代码库的交互方式。
🔒 耦合与内聚
组合增加了容器内部的内聚性。容器成为部分内部逻辑的责任者。这通常有利于封装。然而,它也增加了耦合性。容器在没有部分的情况下无法正常工作。
聚合降低了内聚性。容器依赖于部分,但部分具有独立存在的能力。这可能导致更松散的耦合,使组件更容易独立测试。
🧪 测试策略
这些选择会影响单元测试。
- 组合: 在测试整体时,你通常会隐式地测试部分。模拟部分可能需要重新创建整体的状态。你可能需要测试生命周期逻辑(创建/销毁)。
- 聚合: 你可以轻松地注入一个模拟对象或存根。这个部分是外部的。这有助于独立测试该部分的逻辑,而不受容器逻辑的影响。
📝 决策指南
在设计过程中遇到部分-整体关系时,请提出以下具体问题来确定正确的关系类型。
- 如果没有整体,部分是否仍然有意义?
如果答案是肯定的,倾向于使用聚合。如果是否定的,倾向于使用组合。 - 部分能否属于多个整体?
如果答案是肯定的,必须使用聚合。组合不允许有多个所有者。 - 谁负责部分的创建?
如果整体负责创建它,很可能是组合。如果由外部管理者创建,则很可能是聚合。 - 如果整体被删除,会发生什么?
如果部分必须被删除,使用组合。如果部分必须存活,使用聚合。
🔗 与其他图示类型的关系
复合结构图并非孤立存在。这些关系通常也出现在类图中。
- 类图: 使用聚合和组合来定义类的属性和关联。表示法是相同的。
- 顺序图: 生命周期关系表现为创建消息。组合可能在序列中显示容器向部分发送的“创建”消息。
- 部署图: 物理节点可能聚合软件构件。如果一台服务器托管一个应用程序,这是聚合还是组合?通常为聚合,因为服务器可能托管多个应用程序,且应用程序可以迁移。
🧠 面向对象设计中的细微差别
在现代编程语言中,这些概念对应于特定的设计模式。
依赖注入
依赖注入是一种自然支持聚合的技术。你将依赖项注入构造函数或设置器中。容器并不拥有该依赖项。这有助于提高可测试性和灵活性。
值对象与实体
在领域驱动设计中,值对象通常被组合进实体中。它们自身没有身份,仅存在于实体的上下文中。这是一种典型的组合关系。引用其他实体的实体通常通过聚合来实现(例如,一个客户聚合了多个订单)。
🛡️ 安全性与数据完整性
选择组合可以为数据完整性提供安全保障。通过绑定生命周期,可以确保孤立数据不会积累。例如,如果一个“会话”组合了一个“用户上下文”,关闭会话将确保上下文被清除。如果此处使用聚合,可能会导致过时数据残留在内存或数据库中。
然而,聚合可以防止意外销毁。如果一个“报表生成器”聚合了一个“数据源”,关闭生成器不应清除数据源。数据源必须在生成器的临时故障中存活下来。
🔍 分析现有模型
在审查遗留图示时,你可能会发现模糊之处。如何解释一个不明确的关系?
- 寻找生命周期逻辑: 检查代码或数据库触发器。删除A是否会删除B?这表明是组合关系。
- 寻找共享关系: B是否出现在多个A中?这表明是聚合关系。
- 检查命名规范: 有时“Manager”暗示聚合关系(管理现有资源),而“Builder”暗示组合关系(创建资源)。
🎯 结构完整性总结
在聚合与组合之间做出选择是一项根本性的架构决策。它定义了责任边界以及系统内存在的流动方式。聚合允许灵活性和共享,将部分视为可独立存在的实体,可以被组合在一起。组合则强制执行严格的边界,确保部分是整体不可或缺的组成部分,无法在整体被销毁后继续存在。
通过在复合结构图中严格应用这些概念,您可以创建准确反映软件运行时行为的模型。这种清晰性减少了技术债务,简化了新开发人员的上手过程,并为系统的演进提供了坚实的基础。
始终根据组件的生命周期要求验证您的设计选择。一张绘制得当且使用正确菱形符号的图表,可以在开发周期的后期节省数小时的调试时间和架构困惑。
