
文章目录
📖 开篇导读
在之前的课程中,我们系统学习了面向对象的三大特性:封装、继承、多态。继承让我们能够复用父类的代码,建立起“is-a”的关系。但是,继承并非解决代码复用的唯一方法,有时候过度使用继承会让类层次变得臃肿、脆弱。
在面向对象设计中,类与类之间的关系除了继承(泛化),还有依赖、关联、聚合和组合。理解这些关系,可以帮助你设计出更灵活、更低耦合的系统。特别是组合(Composition)和聚合(Aggregation),它们体现了“has-a”的关系,是现代软件设计中提倡的**“组合优于继承”**原则的核心。
💡 工作场景:
- 组合:订单包含多个商品,商品的生命周期依赖于订单。订单删除,商品项自然消失。
- 聚合:学院包含多个学生,学生离开学院后仍然存在。学院解散,学生还可以转到其他学院。
- 依赖:汽车需要汽油,但汽车不拥有汽油,汽油是从加油站买的。
本课将通过理论讲解和大量代码案例,深入剖析类之间的六种关系,重点讲解组合和聚合的实现与区别,并解释为什么推荐优先使用组合而不是继承。学完本课,你将能够从更高维度设计类的结构,写出高内聚、低耦合的代码。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解类之间六种基本关系(泛化、实现、依赖、关联、聚合、组合) | 面试考点,UML类图基础 |
| 2️⃣ | 掌握依赖、关联、聚合、组合的区别及代码实现 | 写出低耦合代码 |
| 3️⃣ | 理解组合优于继承原则,并能在实际中运用 | 改善代码设计 |
| 4️⃣ | 掌握Python中实现组合和聚合的典型模式 | 构建灵活的结构 |
| 5️⃣ | 能够识别和重构过度继承的设计 | 提升代码可维护性 |
🔥 面试考点:“组合和聚合的区别是什么?”“为什么要说组合优于继承?”“请举例说明依赖、关联和组合的关系。”
📚 知识点理论精讲
一、类与类之间的六种关系
根据UML(统一建模语言),类之间的关系按耦合度由低到高可以分为:
| 关系类型 | 英文 | 描述 | UML符号 | 代码体现 |
|---|---|---|---|---|
| 依赖 | Dependency | 一个类使用另一个类作为参数或局部变量,但不持有引用 | 虚线箭头 | 方法参数、局部变量 |
| 关联 | Association | 一个类持有另一个类的引用,双方相对独立,没有生命周期依赖 | 实线箭头 | 实例变量(引用) |
| 聚合 | Aggregation | 关联的一种特殊形式,表示“整体-部分”关系,部分可以脱离整体独立存在 | 空心菱形箭头 | 实例变量,但整体不负责部分的生命周期 |
| 组合 | Composition | 更强的聚合,部分的生命周期由整体负责,同生共死 | 实心菱形箭头 | 实例变量,在整体构造时创建,析构时销毁 |
| 泛化 | Generalization | 继承关系,子类继承父类,表示“is-a” | 空心三角箭头 | class Child(Parent): |
| 实现 | Realization | 接口与实现类的关系 | 虚线空心三角箭头 | 抽象基类与子类 |
🧠 通俗类比:
- 依赖:你需要在某个方法中使用一把螺丝刀,你去借或者临时买一个,用完就还了(不持有)。
- 关联:你有一个同事,你们都知道彼此,但各自独立生活。
- 聚合:你有一台电脑,电脑里有硬盘。如果电脑坏了,硬盘可以取出装到别的电脑上使用(整体与部分可分离)。
- 组合:你有一部手机,手机里有电池。如果手机报废,电池通常也跟着报废(部分随整体消亡)。
二、依赖(Dependency)
依赖是最弱的关系,一个类在方法中使用了另一个类,但类本身不持有对方的引用。通常通过方法参数、返回值或局部变量体现。
class Printer:
def print(self, document):
print(f"打印: {document}")
class Computer:
def print_document(self, printer: Printer, document):
# 依赖:通过参数传入,不持有
printer.print(document)
三、关联(Association)
关联表示一个类知道另一个类,持有对方的引用,但双方生命周期独立,没有强制的整体-部分约束。关联可以是单向的也可以是双向的。
class Teacher:
def __init__(self, name):
self.name = name
class Student:
def __init__(self, name, teacher=None):
self.name = name
self.teacher = teacher # 关联 Teacher
四、聚合(Aggregation)
聚合表示“整体-部分”的关系,但部分可以脱离整体独立存在。整体不负责部分的生命周期。在代码层面,通常通过构造函数或setter传递外部创建的对象,整体销毁时部分仍然存活。
class Player:
def __init__(self, name):
self.name = name
class Team:
def __init__(self, name):
self.name = name
self.players = [] # 聚合:队伍包含球员
def add_player(self, player: Player):
self.players.append(player)
def remove_player(self, player: Player):
self.players.remove(player)
# 球员可以脱离队伍存在
p1 = Player("张三")
team = Team("战狼队")
team.add_player(p1) # 聚合关系
del team # 队伍销毁,p1仍然存在
五、组合(Composition)
组合是更强的聚合,部分的生命周期完全由整体控制。整体创建时部分也被创建,整体销毁时部分也随之销毁。在代码中,通常整体在__init__中创建部分,不使用外部传入的对象。
class Engine:
def __init__(self, power):
self.power = power
def start(self):
print(f"引擎({self.power}马力)启动")
class Car:
def __init__(self, model, engine_power):
self.model = model
self.engine = Engine(engine_power) # 组合:汽车创建时引擎随之创建
def start(self):
print(f"{self.model} 启动")
self.engine.start()
car = Car("Tesla", 300)
car.start()
del car # 汽车销毁,引擎也随之销毁
💡 工作应用:GUI编程中,窗口(Window)包含按钮(Button)通常使用组合;电商系统中,订单(Order)包含订单项(OrderItem)通常使用组合;公司包含部门,部门可以独立存在,则使用聚合。
六、组合优于继承原则
《设计模式》一书中提出:优先使用对象组合,而不是类继承。
- 继承的缺点:父类变化会影响所有子类;子类不得不继承父类的所有方法,可能导致不必要的方法暴露;继承复用是“白箱复用”,内部细节暴露。
- 组合的优点:灵活,可以在运行时改变行为;只依赖接口,不依赖具体实现;封装性好,是“黑箱复用”。
6.1 继承的典型案例问题
class Bird:
def fly(self):
print("飞行")
class Penguin(Bird):
def fly(self):
raise Exception("企鹅不会飞")
这里企鹅继承鸟,但飞行行为不合适。如果使用组合,可以将“行为”抽取出来,动态赋予。
6.2 组合改进
class Flyable:
def fly(self):
print("飞行")
class NonFlyable:
def fly(self):
print("不会飞")
class Bird:
def __init__(self, flight_behavior):
self.flight_behavior = flight_behavior
def fly(self):
self.flight_behavior.fly()
penguin = Bird(NonFlyable())
eagle = Bird(Flyable())
💻 代码案例实操
案例1:依赖、关联、聚合、组合对比
"""
relationship_demo.py
演示四种关系的代码体现
"""
# 1. 依赖:使用方法参数或局部变量
class Logger:
def log(self, msg):
print(f"[LOG] {msg}")
class Service:
def process(self, logger: Logger, data):
# 依赖:logger作为参数传入,不存储
logger.log(f"处理数据: {data}")
result = f"结果:{data}"
logger.log(f"完成:{result}")
return result
# 2. 关联:持有一个引用,但不负责生命周期
class Address:
def __init__(self, city, street):
self.city = city
self.street = street
class Person:
def __init__(self, name, address=None):
self.name = name
self.address = address # 关联,可以后设置
def set_address(self, addr):
self.address = addr
# 3. 聚合:整体包含部分,部分可独立存在
class CPU:
def __init__(self, model):
self.model = model
class Computer:
def __init__(self, brand):
self.brand = brand
self.cpu = None # 聚合,外部传入或setter
def install_cpu(self, cpu: CPU):
self.cpu = cpu
cpu = CPU("Intel i7")
pc = Computer("Dell")
pc.install_cpu(cpu)
# cpu 可以独立于 pc 存在
# 4. 组合:整体创建并管理部分生命周期
class Wheel:
def __init__(self, size):
self.size = size
class Car:
def __init__(self, brand):
self.brand = brand
# 在构造函数内部直接创建轮子,外部无法独立获取轮子
self.wheels = [Wheel(17) for _ in range(4)]
def rotate_wheels(self):
for w in self.wheels:
print(f"{self.brand} 车轮旋转")
car = Car("Toyota")
car.rotate_wheels()
# 轮子没有暴露到外部,生命周期绑定在车上
案例2:订单-商品组合关系
"""
order_composition.py
演示组合:订单包含订单项,订单销毁时订单项也随之销毁
"""
class OrderItem:
def __init__(self, product_name, price, quantity):
self.product_name = product_name
self.price = price
self.quantity = quantity
def subtotal(self):
return self.price * self.quantity
def __repr__(self):
return f"Item({self.product_name}, {self.quantity}个)"
class Order:
def __init__(self, order_id):
self.order_id = order_id
self.items = [] # 组合:在订单内部创建和管理
def add_item(self, product_name, price, quantity=1):
item = OrderItem(product_name, price, quantity)
self.items.append(item)
print(f"添加商品: {product_name} x{quantity}")
def total(self):
return sum(item.subtotal() for item in self.items)
def display(self):
print(f"订单号: {self.order_id}")
for item in self.items:
print(f" {item}: 小计 {item.subtotal()}元")
print(f"总计: {self.total()}元")
# 析构时演示(实际不用依赖__del__)
def __del__(self):
print(f"订单 {self.order_id} 销毁,订单项也随之销毁")
# 使用
order = Order("ORD-001")
order.add_item("《Python编程》", 89, 2)
order.add_item("鼠标", 45, 1)
order.display()
# 当order被删除,order.items中的OrderItem对象也不再被引用
del order
案例3:部门-员工聚合关系
"""
department_aggregation.py
演示聚合:部门包含员工,但员工可以独立于部门存在
"""
class Employee:
def __init__(self, emp_id, name):
self.emp_id = emp_id
self.name = name
def __repr__(self):
return f"Employee({self.emp_id}, {self.name})"
class Department:
def __init__(self, dept_name):
self.dept_name = dept_name
self.employees = [] # 聚合:员工通过外部添加
def add_employee(self, emp: Employee):
self.employees.append(emp)
print(f"{emp.name} 加入 {self.dept_name}")
def remove_employee(self, emp: Employee):
self.employees.remove(emp)
print(f"{emp.name} 离开 {self.dept_name}")
def count(self):
return len(self.employees)
def __repr__(self):
return f"Department({self.dept_name}, 员工数:{self.count()})"
# 员工独立创建
alice = Employee(1001, "Alice")
bob = Employee(1002, "Bob")
charlie = Employee(1003, "Charlie")
# 部门1
dev_dept = Department("技术部")
dev_dept.add_employee(alice)
dev_dept.add_employee(bob)
print(dev_dept)
# 部门2
hr_dept = Department("人事部")
hr_dept.add_employee(charlie)
# 员工可以换部门
dev_dept.remove_employee(bob)
hr_dept.add_employee(bob)
# 删除部门,员工仍然存在(没有del调用)
del dev_dept
print(f"Alice仍然存在: {alice}")
案例4:组合优于继承——汽车与引擎/轮胎(组合 vs 继承)
"""
composition_vs_inheritance.py
比较使用继承和组合实现代码复用
"""
# 错误示例:通过继承来复用引擎功能
class Engine:
def start(self):
print("引擎启动")
def stop(self):
print("引擎停止")
class ElectricEngine(Engine):
def charge(self):
print("充电")
# 汽车继承引擎?这不合理,汽车不是一种引擎
class BadCar(Engine):
def drive(self):
self.start()
print("行驶")
self.stop()
# 正确示例:汽车组合引擎和轮胎
class GoodEngine:
def start(self):
print("引擎启动")
def stop(self):
print("引擎停止")
class ElectricEngine(GoodEngine):
def charge(self):
print("充电")
class Tire:
def __init__(self, size):
self.size = size
def rotate(self):
print(f"轮胎({self.size})旋转")
class GoodCar:
def __init__(self, engine, tire_size=17):
self.engine = engine # 组合:引擎从外部传入
self.tires = [Tire(tire_size) for _ in range(4)]
def start(self):
self.engine.start()
def drive(self):
self.start()
for tire in self.tires:
tire.rotate()
print("汽车行驶")
def stop(self):
self.engine.stop()
# 使用组合:可以灵活替换引擎
gas_engine = Engine()
electric_engine = ElectricEngine()
car1 = GoodCar(gas_engine)
car2 = GoodCar(electric_engine, 18)
car1.drive()
car2.drive()
案例5:图形编辑器中的组合——ShapeGroup
"""
shape_group_composition.py
使用组合模式(也是一种组合关系)实现图形组
"""
from abc import ABC, abstractmethod
class Graphic(ABC):
@abstractmethod
def draw(self):
pass
@abstractmethod
def move(self, dx, dy):
pass
class Circle(Graphic):
def __init__(self, x, y, r):
self.x = x
self.y = y
self.r = r
def draw(self):
print(f"画圆 圆心({self.x},{self.y}) 半径{self.r}")
def move(self, dx, dy):
self.x += dx
self.y += dy
class Rectangle(Graphic):
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
def draw(self):
print(f"画矩形 左上({self.x},{self.y}) 宽{self.w}高{self.h}")
def move(self, dx, dy):
self.x += dx
self.y += dy
class Group(Graphic):
"""组合图形:可以包含多个图形,并作为一个整体操作"""
def __init__(self):
self.shapes = []
def add(self, shape: Graphic):
self.shapes.append(shape)
def remove(self, shape: Graphic):
self.shapes.remove(shape)
def draw(self):
for shape in self.shapes:
shape.draw()
def move(self, dx, dy):
for shape in self.shapes:
shape.move(dx, dy)
# 使用
c1 = Circle(10,10,5)
c2 = Circle(20,20,3)
r1 = Rectangle(30,30,10,20)
group1 = Group()
group1.add(c1)
group1.add(r1)
group2 = Group()
group2.add(c2)
root_group = Group()
root_group.add(group1)
root_group.add(group2)
print("原始位置")
root_group.draw()
root_group.move(5,5)
print("\n移动后")
root_group.draw()
案例6:依赖注入(依赖关系的解耦)
"""
dependency_injection.py
演示如何通过依赖注入降低耦合
"""
class EmailService:
def send(self, to, subject, body):
print(f"发送邮件给 {to}: {subject} - {body}")
class SMSService:
def send(self, phone, message):
print(f"发送短信给 {phone}: {message}")
class NotificationManager:
# 依赖注入:通过构造器传入依赖
def __init__(self, message_service):
self.service = message_service # 依赖抽象接口,不依赖具体实现
def notify(self, recipient, content):
# 假设统一接口 send
self.service.send(recipient, "通知", content)
# 可以注入不同的服务
email_svc = EmailService()
sms_svc = SMSService()
notify_with_email = NotificationManager(email_svc)
notify_with_sms = NotificationManager(sms_svc)
notify_with_email.notify("user@example.com", "您的订单已发货")
notify_with_sms.notify("13800138000", "您的验证码是123456")
案例7:综合实战——汽车工厂(组合、聚合、依赖)
"""
car_factory_demo.py
综合使用多种关系构建汽车生产系统
"""
class Part:
"""零件基类"""
def __init__(self, name):
self.name = name
class Engine(Part):
def start(self):
print(f"{self.name} 引擎启动")
class Wheel(Part):
def rotate(self):
print(f"{self.name} 车轮旋转")
class Car:
"""汽车使用组合方式包含零件"""
def __init__(self, model, engine_type, wheel_size):
self.model = model
# 组合:引擎和车轮在汽车内部创建
self.engine = Engine(f"{model}-Engine")
self.wheels = [Wheel(f"{model}-Wheel{i}") for i in range(4)]
self.wheel_size = wheel_size # 简单属性
def drive(self):
self.engine.start()
for w in self.wheels:
w.rotate()
print(f"{self.model} 行驶")
class Worker:
"""工人,与工厂是聚合关系"""
def __init__(self, name):
self.name = name
def assemble(self, car: Car):
print(f"工人 {self.name} 正在组装 {car.model}")
class Factory:
"""工厂,包含工人(聚合),依赖零件供应商"""
def __init__(self, name):
self.name = name
self.workers = [] # 聚合:工人可独立于工厂
def hire(self, worker: Worker):
self.workers.append(worker)
def produce_car(self, model, engine_type, wheel_size):
# 依赖:使用传入的零件参数(实际上零件在Car内部创建)
car = Car(model, engine_type, wheel_size)
# 随机找一个工人组装(依赖 Worker)
if self.workers:
worker = self.workers[0]
worker.assemble(car)
else:
print("没有工人,无法生产")
return car
# 创建工人
w1 = Worker("张三")
w2 = Worker("李四")
# 创建工厂
factory = Factory("特斯拉超级工厂")
factory.hire(w1)
factory.hire(w2)
# 生产汽车
car1 = factory.produce_car("Model 3", "electric", 18)
car2 = factory.produce_car("Model Y", "electric", 20)
car1.drive()
car2.drive()
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 混淆聚合和组合 | 生命周期管理错误,可能导致内存泄漏或提前释放 | 记住:组合强调整体部分同生共死,聚合部分可独立 |
| 2 | 在组合关系中,从外部传入部分对象 | 部分的生命周期不由整体控制,变成了聚合 | 组合应该在整体内部创建部分,不通过参数传入 |
| 3 | 依赖关系用实例变量持有 | 提高了耦合度,关系变成了关联 | 能通过参数传递就不要存储为成员变量 |
| 4 | 过度使用继承,造成类层次过深 | 代码难以维护,修改父类影响所有子类 | 优先考虑组合,使用“策略”等设计模式 |
| 5 | 双向关联导致循环引用 | 垃圾回收困难,代码复杂度高 | 尽量使用单向关联,必要时使用弱引用 |
| 6 | 在__del__中依赖组合对象 | 析构顺序不确定,可能导致异常 | 使用上下文管理器或显式关闭方法 |
| 7 | 组合模式中忘记递归调用 | 对组合对象的操作无法递归到子对象 | 在组合类的方法中遍历所有子对象并调用相同方法 |
| 8 | 依赖注入时没有提供接口抽象 | 具体类变化导致多处修改 | 依赖抽象基类或协议(Protocol) |
| 9 | 聚合中部分被多个整体共享时未处理共享语义 | 删除一个整体可能意外删除部分 | 使用引用计数或共享所有权概念 |
| 10 | 误认为所有包含关系都是组合 | 不适合的场景使用了强制生命周期耦合 | 根据业务需求选择:部分离开整体还有意义吗? |
📝 课后实战练习题
第1题:识别关系
分析以下代码片段,指出属于哪种关系(依赖、关联、聚合、组合、继承):
class A:
def method(self, b): pass
class C:
def __init__(self): self.d = D()
class E:
def __init__(self, f): self.f = f
class G(H): pass
第2题:实现购物车的组合关系
定义CartItem(商品名、单价、数量)和ShoppingCart(包含__init__中创建空列表,提供添加、删除、计算总价)。确保购物车销毁时,CartItem也被销毁(虽然Python GC会处理)。然后实例化购物车,添加商品,打印总价。
第3题:班级与学生的聚合关系
定义Student类(姓名、学号)和Class类(班级名称,包含学生列表,提供添加、移除学生)。学生可以独立于班级存在。创建几个学生,加入班级,然后解散班级,学生仍然存在。
第4题:组合优于继承的重构
现有继承关系:Bird -> FlyBird、SwimBird等。尝试用组合模式重构:定义FlyBehavior和SwimBehavior接口,Bird组合这些行为。示例:一只企鹅(不能飞能游泳),一只燕子(能飞不能游泳)。
第5题:依赖注入实践
实现一个EmailSender接口,send(to, subject, body)。实现SmtpEmailSender和MockEmailSender(只打印)。写一个UserService类,通过构造函数注入EmailSender。在register_user方法中调用发送欢迎邮件。测试两种实现。
第6题:公司-部门-员工三层关系
设计Company(聚合部门)、Department(聚合员工)、Employee。要求:
- 部门可以独立于公司存在(先创建部门再加入公司)。
- 员工可以独立于部门存在。
- 公司解散时,部门仍然存在(聚合);部门解散时,员工仍然存在。
- 实现添加、移除、查询功能。
第7题:组合模式实现文件系统
使用组合模式实现文件和文件夹树:
File有name和size,提供display(indent)Folder有name和children列表,可以add和remove,display时递归调用子项
测试创建一个包含子文件夹和文件的文件夹树,并显示整个结构。
🧠 知识点思维导图总结
🔜 下节课预告
本课学习了类与类之间的关系,特别是组合和聚合。下一节课我们将进行面向对象综合实战,开发一个完整的人物信息管理系统,将封装、继承、多态、组合、设计模式等融会贯通。
第30课:OOP综合实战:人物信息管理系统控制台完整项目开发
内容包括:
- 需求分析与类设计
- 使用继承构建人物类型体系
- 使用组合管理关系(如部门、联系人)
- 实现完整的CRUD操作
- 数据持久化(文件)
- 菜单交互与异常处理
这将是一个完整的OOP项目,让你将所学知识融会贯通,达到进阶水平。
🌟 学习鼓励:理解类之间的关系是面向对象设计的进阶。组合和聚合虽然看起来简单,但在实际项目中正确应用它们能大大提升代码质量。请多思考“这个部分脱离整体还能独立存在吗?”从而选择组合还是聚合。动手完成练习,你将逐步掌握OOD的精髓。
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

444

被折叠的 条评论
为什么被折叠?



