📄 本篇摘要
类与类的关系按【黏度】从弱到强排一排:依赖(用过就走)→ 关联(长期认识)→ 聚合(整体留着部分,部分也能独立活)→ 组合(整体亡则部分亡)→ 泛化(继承,is-a)。判据只有一个:部分对象的生命周期归谁管。
一、五级关系光谱
| 关系 | UML 记号 | 一句话 | 例子 |
|---|---|---|---|
| 依赖 | 虚线箭头 | 方法参数里用一下 | 函数里调了 hash() |
| 关联 | 实线箭头 | 长期持有一个引用 | 订单记着客户 |
| 聚合 | 空心菱形 | has-a,但部分可独立活 | 车队和学生(学生退队不死) |
| 组合 | 实心菱形 | has-a,部分随整体同生共死 | 订单和订单行(删订单行必删) |
| 泛化 | 空心三角 | is-a,继承(5-16) | 狗是动物 |
判分标准=生命周期归属:部分的生灭由整体掌控(建订单时建行、删订单时删行)→ 组合;部分独立存在(学生不因退队而消失)→ 聚合。分不清时问一句:删了整体,部分要不要一起删?
二、为什么这个区别重要
关系强度决定改动波及面:依赖最弱(改哈希函数不用通知你);组合最强(订单行格式一变,订单跟着动)。设计的目标:能用弱关系就不用强关系——组合过深=牵一发动全身,这正是 5-11 重构时【霰弹式修改】坏味道的根源。
三、动手 Demo:聚合与组合一测便知
class Student:
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
class Team: # 聚合:学生先存在,入队而已
def __init__(self, title):
self.title = title
self.members = []
def join(self, s):
self.members.append(s)
class Order: # 组合:订单行随订单生灭
def __init__(self, no, items):
self.no = no
self.lines = [{"item": n, "qty": q} for n, q in items]
def total(self):
return sum(l["qty"] for l in self.lines)
# 聚合侧
b = Student("波仔"); x = Student("小红")
team = Team("编程小组")
team.join(b); team.join(x)
del team # 团队没了……
print("学生还活着:", b, x)
# 组合侧
order = Order("SO-1", [("键盘", 2), ("鼠标", 3)])
print("订单行总数:", order.total())
del order # 订单没了……
# order 的订单行无处可达,随之消亡 —— 生命周期被整体掌控
print("订单行随订单消亡(无可达引用)")
沙箱实跑输出:
学生还活着: 波仔 小红
订单行总数: 5
订单行随订单消亡(无可达引用)
看到什么算成功:删掉 Team 后两个学生引用依然可用;Order 消亡后其订单行无法再访问。
读结果:Team 对 Student 是聚合——只存引用,成员在队外本来就存在;Order 对订单行是组合——行在构造订单时才创建、随订单一起被回收。代码上的判据:构造函数里创建的=组合;外面传入的=聚合。
四、30 秒实验
把 Team.join 的成员改存 s.name(字符串副本)——引用断了,改名字团队不知道。聚合传引用不传拷贝:拷贝出的第二条记录就是 5-11 说的重复数据隐患。
五、常见误区
| 误区 | 真相 |
|---|---|
| 聚合和组合随便选 | 生命周期归属是硬判据;选错则删除行为(级联删)全乱 |
| 继承和组合是竞争关系 | 不同维度:泛化表 is-a、聚合组合表 has-a;先分清哪种关系再选工具 |
| 关系画了图就算设计完 | 关系强度决定改动波及面——弱关系优先是设计目标 |
自测三问(点击展开)
1. **聚合与组合的判据?** 答:部分对象的生命周期是否被整体掌控——掌控是组合(实心菱形),独立是聚合(空心菱形)。 2. **五级关系按黏度从弱到强?** 答:依赖 → 关联 → 聚合 → 组合 →(泛化另维)。 3. **代码上怎么快速分辨组合与聚合?** 答:整体构造函数里创建部分=组合;部分从外部传入=聚合。- ☐ 5-21 学习完成
相关笔记
- 继承与多重继承 —— 泛化(is-a)的展开
- C++ 10.4.5 · 嵌套类 —— 组合的一种极端形态
- ⌨️ 编程原理 · 索引 —— 全课路线图