8、组合与聚合:对象之间的“有”关系,组合 vs 继承的选择策略,实战案例:汽车与引擎
一、对象之间的“有”关系到底是什么?
面向对象编程里,除了“is a”(继承),还有一种更常见的关系叫“has a”。说白了,就是一个对象里面装着另一个对象。比如汽车有引擎,学校有学生,电脑有CPU。
这种“有”关系,在代码里通常表现为一个类的成员变量是另一个类的对象。但同样是“有”,其实还分两种:组合和聚合。嗯,这里要注意,很多人把这两个混为一谈,但它们的生命周期和所有权完全不同。
二、组合 vs 聚合:一张表说清楚
| 对比维度 | 组合(Composition) | 聚合(Aggregation) |
|---|---|---|
| 生命周期 | 同生共死。整体销毁,部分也销毁。 | 各自独立。整体销毁,部分依然存在。 |
| 所有权 | 整体拥有部分,部分不能共享。 | 整体引用部分,部分可以共享。 |
| 代码体现 | 成员对象在构造函数中直接 new 出来。 | 成员对象通过参数传入,或从外部赋值。 |
| 典型例子 | 汽车与引擎。引擎是汽车不可分割的一部分。 | 学校与学生。学生离开学校后依然存在。 |
你想想看,汽车报废了,引擎还能拆下来单独用吗?理论上可以,但在组合关系里,我们默认引擎是汽车专属的,车没了引擎也就没意义了。而聚合呢?学生毕业了,学校还在,学生也还在,只是关系断了。
我个人习惯:判断用组合还是聚合,就问自己一句话——“整体没了,部分还有存在的价值吗?” 如果答案是否定的,用组合;如果是肯定的,用聚合。
三、组合 vs 继承:什么时候该选谁?
很多新手一上来就喜欢用继承,觉得“子类复用父类代码”很爽。但我在项目中遇到过好几次,继承层级一深,改一个父类方法,所有子类都跟着遭殃。说白了,继承是一种强耦合,而组合是一种松耦合。
这里我总结了一个简单的选择策略:
- 能用组合,就别用继承。组合更灵活,运行时可以动态替换行为。
- 继承只用在“is a”关系明确且稳定时。比如“猫是动物”,这个关系不会变。
- 如果只是为了复用代码,优先考虑组合。把公共逻辑抽成一个工具类或策略类,然后通过成员变量引用它。
避坑指南:我曾经在一个项目中用继承实现了“汽车”和“电动车”的关系,结果后来要加“混动车”,发现既不能继承汽车也不能继承电动车,搞得代码一团糟。后来重构为组合,把“动力系统”作为一个接口,汽车里组合一个动力系统对象,问题迎刃而解。
四、实战案例:汽车与引擎
我们直接看代码。先定义一个引擎类:
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
self.running = False
def start(self):
self.running = True
print(f"引擎启动,马力:{self.horsepower}")
def stop(self):
self.running = False
print("引擎关闭")
然后定义汽车类,这里用的是组合关系:
class Car:
def __init__(self, brand, horsepower):
self.brand = brand
# 组合:引擎在汽车构造时直接创建,同生共死
self.engine = Engine(horsepower)
def drive(self):
if not self.engine.running:
self.engine.start()
print(f"{self.brand} 正在行驶...")
def stop(self):
self.engine.stop()
print(f"{self.brand} 已停车")
使用起来很简单:
my_car = Car("特斯拉", 500)
my_car.drive() # 引擎启动,特斯拉正在行驶...
my_car.stop() # 引擎关闭,特斯拉已停车
你看,汽车对象销毁时,引擎对象也跟着没了。这就是组合的生命周期绑定。
那聚合怎么写呢?我们换个例子——学校与学生:
class Student:
def __init__(self, name):
self.name = name
class School:
def __init__(self, name):
self.name = name
self.students = [] # 聚合:学生列表从外部添加
def add_student(self, student):
self.students.append(student)
print(f"学生 {student.name} 加入 {self.name}")
# 学生对象独立存在
s1 = Student("张三")
s2 = Student("李四")
school = School("第一中学")
school.add_student(s1)
school.add_student(s2)
# 即使 school 被销毁,s1 和 s2 依然存在
五、一张图看懂组合与聚合的核心区别
下面我用 SVG 画了一张结构图,帮你直观理解两者的差异:
六、总结一下我的经验
组合与聚合,说白了就是“拥有”和“使用”的区别。组合是“我拥有你,你属于我”,聚合是“我用你,但你是自由的”。
在实际项目中,我建议你优先考虑组合。为什么?因为组合的封装性更好,对象之间的依赖更清晰。聚合虽然灵活,但容易造成对象关系混乱,特别是当多个对象共享同一个部分时,修改那个部分可能会影响所有引用它的对象。
注意:不要为了追求“松耦合”而滥用聚合。如果一个部分对象只属于一个整体,并且生命周期完全由整体控制,那就用组合。强行用聚合只会让代码更难维护。
最后,记住一句话:继承是白盒复用,组合是黑盒复用。白盒复用暴露细节,黑盒复用只暴露接口。哪个更安全?不言而喻。
公众号:蓝海资料掘金营,微信deep3321