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 画了一张结构图,帮你直观理解两者的差异:

组合(Composition) 汽车 引擎 同生共死 聚合(Aggregation) 学校 学生 各自独立 实心菱形 = 组合(整体拥有部分) 空心菱形 = 聚合(整体引用部分) 组合:self.engine = Engine(horsepower) 聚合:self.students = [] # 外部传入

六、总结一下我的经验

组合与聚合,说白了就是“拥有”和“使用”的区别。组合是“我拥有你,你属于我”,聚合是“我用你,但你是自由的”。

在实际项目中,我建议你优先考虑组合。为什么?因为组合的封装性更好,对象之间的依赖更清晰。聚合虽然灵活,但容易造成对象关系混乱,特别是当多个对象共享同一个部分时,修改那个部分可能会影响所有引用它的对象。

注意:不要为了追求“松耦合”而滥用聚合。如果一个部分对象只属于一个整体,并且生命周期完全由整体控制,那就用组合。强行用聚合只会让代码更难维护。

最后,记住一句话:继承是白盒复用,组合是黑盒复用。白盒复用暴露细节,黑盒复用只暴露接口。哪个更安全?不言而喻。


公众号:蓝海资料掘金营,微信deep3321