Day 39 — 设计模式(结构型)
结构型设计模式关注如何组合类和对象以形成更大的结构,在不破坏封装的前提下实现灵活的系统架构。
目录
1. 结构型模式概述
1.1 什么是结构型模式?
结构型模式描述如何将类或对象组合在一起形成更大的结构。它们解决的是组装问题——如何让两个独立的类协同工作,如何给对象添加新功能而不修改其代码,如何管理对象的访问权限。
┌─────────────────────────────────────────────────────┐
│ 结构型模式 │
├─────────────────────────────────────────────────────┤
│ 目的:组合类和对象形成更大结构,保持灵活和高效 │
│ │
│ 核心思想:松耦合 + 高内聚 │
│ 关键原则:组合优于继承 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 适配器 │ │ 代理 │ │ 装饰器 │ │
│ │ 让不兼容 │ │ 控制访问 │ │ 动态加 │ │
│ │ 的接口 │ │ 和延迟 │ │ 功能 │ │
│ │ 协同工作 │ │ 加载 │ │ 到对象 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
1.2 为什么需要结构型模式?
| 场景 | 问题 | 模式解决 |
|---|---|---|
| 第三方库接口不匹配 | 无法直接集成 | 适配器模式包装接口 |
| 大对象频繁创建开销大 | 性能下降 | 代理模式延迟加载 |
| 需要给对象动态加功能 | 子类爆炸 | 装饰器模式堆叠功能 |
| 需要控制对象访问权限 | 安全隐患 | 代理模式控制访问 |
1.3 结构型模式 vs 创建型模式
| 维度 | 创建型模式 | 结构型模式 |
|---|---|---|
| 关注点 | 对象的创建过程 | 对象的组合方式 |
| 典型代表 | 单例、工厂、建造者 | 适配器、代理、装饰器 |
| 解决问题 | "谁负责创建对象" | "对象如何协同工作" |
| 封装对象 | 创建逻辑 | 组合逻辑 |
2. 适配器模式(Adapter)
2.1 概念与定义
适配器模式将一个类的接口转换成客户希望的另一个接口,使原本接口不兼容的类可以协同工作。
类比:手机充电器上的电源适配器——美标插头(110V)到国标(220V)的转换器。适配器充当中间层,一端适配插座,一端适配手机。
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ 客户端 │────▶│ 适配器 │────▶│ 第三方 │
│ (期待 │ │ (接口转换) │ │ (不兼容) │
│ 标准接口)│ └──────────────┘ └──────────┘
└──────────┘
2.2 原理解析
适配器模式的核心机制:
- 目标接口(Target):客户端期望的接口
- 适配者(Adaptee):需要被适配的现有类
- 适配器(Adapter):在目标接口和适配者之间建立桥梁
实现方式:
方式一:类适配器(多重继承)
class Adapter(Target, Adaptee):
def request(self):
# 使用继承来的 Adaptee 方法
self.specific_request()
Python 支持多重继承,但这种方式耦合度高,不推荐。
方式二:对象适配器(组合,推荐)
class Adapter(Target):
def __init__(self, adaptee):
self._adaptee = adaptee # 组合
def request(self):
self._adaptee.specific_request() # 委派
使用组合而非继承,更灵活、松耦合。推荐使用这种方式。
2.3 使用场景
| 场景 | 说明 |
|---|---|
| 集成第三方库 | SDK 接口与项目不匹配 |
| 旧系统改造 | 新代码需要调用遗留 API |
| 统一多个不同实现 | 多个数据库驱动、支付接口 |
| 适配不同数据格式 | XML → JSON,CSV → 对象 |
2.4 API 速查
| 组件 | 角色 | 说明 |
|---|---|---|
Target |
目标接口 | 定义客户端期待的接口 |
Adaptee |
被适配者 | 已有但接口不兼容的类 |
Adapter |
适配器 | 实现 Target,组合 Adaptee |
2.5 代码示例框架
from abc import ABC, abstractmethod
class Target(ABC):
"""目标接口:客户端期待的标准接口"""
@abstractmethod
def request(self) -> str:
pass
class Adaptee:
"""被适配者:接口不兼容的类"""
def specific_request(self) -> str:
return "特定格式的数据"
class Adapter(Target):
"""适配器:接口转换桥接"""
def __init__(self, adaptee: Adaptee):
self._adaptee = adaptee
def request(self) -> str:
return self._adaptee.specific_request()
3. 代理模式(Proxy)
3.1 概念与定义
代理模式为另一个对象提供一个替身或占位符,以控制对该对象的访问。
类比:公司前台(代理)→ 老板(真实对象)。所有访客先找前台,前台决定是否可以见老板。前台可以拦截推销、过滤风险、安排预约。
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ 客户端 │────▶│ 代理 │────▶│ 真实对象 │
│ │ │ (管控访问) │ │ │
└──────────┘ └──────────────┘ └──────────┘
│
┌────┴────┐
│ 控制逻辑 │
│ • 访问权限│
│ • 延迟加载│
│ • 日志记录│
│ • 结果缓存│
└─────────┘
3.2 原理解析
代理和被代理对象实现相同的接口,客户端不知道(也不关心)自己是在和代理还是真实对象通信。
客户端的视角:
┌─────────────────────┐
│ 接口(Interface) │ ← 客户端只知道这个接口
├─────────────────────┤
│ + request() │
└─────────────────────┘
▲
│ 实现
┌─────────┴──────────┐
│ │
┌──────────────┐ ┌──────────────┐
│ Proxy │ │ RealSubject │
│ + request() │ │ + request()│
│ 做额外操作后 │ │ 做真实工作 │
│ 调用真实对象 │ │ │
└──────────────┘ └──────────────┘
3.3 常见代理类型
| 类型 | 用途 | 说明 |
|---|---|---|
| 虚拟代理 | 延迟加载 | 创建开销大的对象延迟到真正使用时 |
| 保护代理 | 权限控制 | 根据权限决定是否允许访问 |
| 远程代理 | 网络封装 | 隐藏远程调用的网络细节 |
| 缓存代理 | 结果缓存 | 缓存计算结果避免重复计算 |
| 日志代理 | 操作记录 | 记录方法调用的参数、时间、结果 |
3.4 使用场景
| 场景 | 代理类型 | 示例 |
|---|---|---|
| 大图片懒加载 | 虚拟代理 | 先显示占位图,真正用到时加载 |
| API 限流 | 保护代理 | 控制第三方 API 调用频率 |
| 分布式服务 | 远程代理 | gRPC stub |
| 计算结果缓存 | 缓存代理 | 缓存数据库查询结果 |
3.5 代码示例框架
from abc import ABC, abstractmethod
class Subject(ABC):
"""抽象主题:定义真实对象和代理的共同接口"""
@abstractmethod
def request(self) -> None:
pass
class RealSubject(Subject):
"""真实主题:真正执行业务逻辑的对象"""
def request(self) -> None:
print("真实对象处理请求")
class Proxy(Subject):
"""代理:控制对真实对象的访问"""
def __init__(self, real_subject: RealSubject):
self._real = real_subject
def request(self) -> None:
if self.check_access():
self._real.request()
self.log_access()
def check_access(self) -> bool:
# 访问控制逻辑
return True
def log_access(self) -> None:
print("代理记录访问日志")
4. 装饰器模式(Decorator)
4.1 概念与定义
装饰器模式动态地将新职责附加到对象上,比继承更灵活——可以无限堆叠功能。
类比:咖啡店点单系统。基础咖啡(美式/拿铁)→ 添加配料(加奶泡/加焦糖/加奶油)。配料就是装饰器,可以层层叠加。
基础咖啡 加奶泡 加焦糖
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ 美式咖啡 │ │ 奶泡装饰器 │ │ 焦糖装饰器 │
│ cost: 22 │ │ +5 元 │ │ +3 元 │
│ desc: 美式 │ │ 包装咖啡 │ │ 再次包装 │
└────────────┘ └──────────────┘ └──────────────┘
│
客户端:美式+奶泡+焦糖=30元
4.2 原理解析
装饰器模式的结构:
┌──────────────┐
│ Component │ ← 抽象组件
│ + operation()│
└──────────────┘
▲
┌────────────┴────────────┐
│ │
┌────────────────┐ ┌──────────────────┐
│ ConcreteComp │ │ Decorator │ ← 抽象装饰器
│ + operation() │ │ - component │
└────────────────┘ │ + operation() │
▲ └──────────────────┘
│ ▲
│ ┌───────┴───────┐
│ │ │
┌────────────────┐ ┌────────────────┐
│ ConcreteDecA │ │ ConcreteDecB │
│ + operation() │ │ + operation() │
└────────────────┘ └────────────────┘
核心机制:
1. 装饰器和被装饰对象实现相同接口
2. 装饰器持有对被装饰对象的引用(组合)
3. 装饰器在调用被装饰对象前后添加额外逻辑
4. 可以多层装饰,每一层都包装上一层
4.3 Python 内置装饰器 vs 装饰器模式
很多人混淆这两个概念,它们完全不同:
| 维度 | Python 装饰器(语法糖) | 装饰器模式(设计模式) |
|---|---|---|
| 本质 | 语法糖(高阶函数) | 设计模式(对象组合) |
| 操作目标 | 函数/方法 | 任何实现了接口的对象 |
| 实现 | @decorator 语法 |
类组合 + 接口继承 |
| 灵活性 | 装饰后不可动态增减 | 运行时动态增减 |
| 应用 | 日志、计时、缓存 | GUI 组件、流处理、订单系统 |
Python 装饰器:编译时绑定
@log → log(func) → 新函数(不可逆)
def foo(): pass
装饰器模式:运行时组合
base = Coffee()
with_milk = MilkDecorator(base)
with_sugar = SugarDecorator(with_milk) # 可以再叠加
4.4 使用场景
| 场景 | 说明 |
|---|---|
| 动态添加功能 | 给对象按需添加日志、缓存、权限检查 |
| 避免子类爆炸 | 10 种功能排列组合 → 不需要 1024 个子类 |
| 运行时调整 | 用户可在运行中勾选/取消功能 |
| 数据流处理 | 对数据流进行层层加工(压缩→加密→发送) |
4.5 API 速查
| 组件 | 角色 | 说明 |
|---|---|---|
Component |
抽象组件 | 定义对象接口 |
ConcreteComponent |
具体组件 | 基础功能实现 |
Decorator |
抽象装饰器 | 维护对 Component 的引用 |
ConcreteDecorator |
具体装饰器 | 添加额外功能 |
5. 三种模式对比
| 维度 | 适配器 | 代理 | 装饰器 |
|---|---|---|---|
| 目的 | 接口转换 | 访问控制 | 功能扩展 |
| 包装对象 | 改变接口 | 保持接口 | 扩展接口 |
| 创建时机 | 编译时就确定 | 运行时 | 运行时 |
| 客户端感知 | 知道有适配器 | 可能不知道是代理 | 知道是装饰后的对象 |
| 示例 | USB-C → HDMI | VPN 代理 | 咖啡加料 |
选择决策树:
需要改接口吗?─────────是──────▶ 适配器模式
否
需要控制访问吗?───────是──────▶ 代理模式
否
需要动态加功能吗?──────是──────▶ 装饰器模式
否
可能不需要结构型模式 😄
6. 实战:缓存代理
设计一个缓存代理系统,对计算密集型操作进行结果缓存。
场景描述
有一个数据查询服务,每次查询都从远程 API 获取数据(模拟为 2 秒延迟)。
使用缓存代理后:
- 首次查询:调用真实服务,结果存入缓存
- 后续相同查询:直接从缓存返回,速度提升 100 倍
架构设计
┌──────────┐ ┌─────────────────┐ ┌──────────────┐
│ 客户端 │───▶│ CacheProxy │───▶│ DataService │
│ │ │ (缓存代理) │ │ (真实服务) │
└──────────┘ │ │ └──────────────┘
│ ┌───────────┐ │
│ │ 缓存 │ │ 查询 "user_001"
│ │ dict: │──┼──────────────┐
│ │ id → data │ │ │
│ └───────────┘ │ ▼
│ │ DataService.query()
│ │ → 2秒后返回结果
│ │
│ 第二次查询同ID │ 查询 "user_001"
│ 直接从缓存返回 │───────✅ 瞬间返回
└─────────────────┘
关键代码
完整实现在 code/03-cache-proxy.py 中,核心结构如下:
class DataService:
"""真实数据服务(模拟远程调用,耗时 2s)"""
def get_data(self, item_id):
time.sleep(2) # 模拟网络延迟
return {"id": item_id, "data": f"数据_{item_id}"}
class CacheProxy:
"""缓存代理:第一次调用真实服务,后续命中缓存"""
def __init__(self, service):
self._service = service
self._cache = {}
def get_data(self, item_id):
if item_id not in self._cache:
self._cache[item_id] = self._service.get_data(item_id)
print(f" [缓存] 未命中,调入数据: {item_id}")
else:
print(f" [缓存] 命中!{item_id}")
return self._cache[item_id]
7. 思考题
-
适配器 vs 外观模式:适配器是改写接口,外观是简化接口。如果要封装一个复杂的子系统提供简单入口,应该用哪个?
-
缓存失效:缓存代理在实战中最大的挑战是缓存失效策略。你会如何设计一个带 TTL(过期时间)的缓存代理?
-
多层代理:如果既要缓存又要权限检查又要日志记录,如何组合多个代理?是链式代理还是用装饰器模式?
-
Python 装饰器 vs 装饰器模式:你有一个类,需要在运行时决定是否添加日志功能,用 Python
@decorator还是装饰器模式?为什么? -
单例代理:代理模式中,代理对象本身是否应该是单例?什么情况下是,什么情况下不是?