← 返回总目录

Day 39 — 设计模式(结构型)

面向对象编程 440 行 3 个可运行脚本

Day 39 — 设计模式(结构型)

结构型设计模式关注如何组合类和对象以形成更大的结构,在不破坏封装的前提下实现灵活的系统架构。


目录

  1. 结构型模式概述
  2. 适配器模式(Adapter)
  3. 代理模式(Proxy)
  4. 装饰器模式(Decorator)
  5. 三种模式对比
  6. 实战:缓存代理
  7. 思考题

1. 结构型模式概述

1.1 什么是结构型模式?

结构型模式描述如何将类或对象组合在一起形成更大的结构。它们解决的是组装问题——如何让两个独立的类协同工作,如何给对象添加新功能而不修改其代码,如何管理对象的访问权限。

┌─────────────────────────────────────────────────────┐
│                    结构型模式                         │
├─────────────────────────────────────────────────────┤
│  目的:组合类和对象形成更大结构,保持灵活和高效        │
│                                                      │
│  核心思想:松耦合 + 高内聚                            │
│  关键原则:组合优于继承                               │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │ 适配器    │  │  代理    │  │  装饰器  │          │
│  │ 让不兼容  │  │ 控制访问 │  │ 动态加   │          │
│  │ 的接口    │  │ 和延迟   │  │ 功能     │          │
│  │ 协同工作  │  │ 加载     │  │ 到对象   │          │
│  └──────────┘  └──────────┘  └──────────┘          │
└─────────────────────────────────────────────────────┘

1.2 为什么需要结构型模式?

场景 问题 模式解决
第三方库接口不匹配 无法直接集成 适配器模式包装接口
大对象频繁创建开销大 性能下降 代理模式延迟加载
需要给对象动态加功能 子类爆炸 装饰器模式堆叠功能
需要控制对象访问权限 安全隐患 代理模式控制访问

1.3 结构型模式 vs 创建型模式

维度 创建型模式 结构型模式
关注点 对象的创建过程 对象的组合方式
典型代表 单例、工厂、建造者 适配器、代理、装饰器
解决问题 "谁负责创建对象" "对象如何协同工作"
封装对象 创建逻辑 组合逻辑

2. 适配器模式(Adapter)

2.1 概念与定义

适配器模式将一个类的接口转换成客户希望的另一个接口,使原本接口不兼容的类可以协同工作。

类比:手机充电器上的电源适配器——美标插头(110V)到国标(220V)的转换器。适配器充当中间层,一端适配插座,一端适配手机。

┌──────────┐     ┌──────────────┐     ┌──────────┐
│ 客户端   │────▶│   适配器     │────▶│ 第三方   │
│ (期待    │     │ (接口转换)   │     │ (不兼容) │
│  标准接口)│     └──────────────┘     └──────────┘
└──────────┘

2.2 原理解析

适配器模式的核心机制:

  1. 目标接口(Target):客户端期望的接口
  2. 适配者(Adaptee):需要被适配的现有类
  3. 适配器(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 代码示例框架

python
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 代码示例框架

python
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 中,核心结构如下:

python
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. 思考题

  1. 适配器 vs 外观模式:适配器是改写接口,外观是简化接口。如果要封装一个复杂的子系统提供简单入口,应该用哪个?

  2. 缓存失效:缓存代理在实战中最大的挑战是缓存失效策略。你会如何设计一个带 TTL(过期时间)的缓存代理?

  3. 多层代理:如果既要缓存又要权限检查又要日志记录,如何组合多个代理?是链式代理还是用装饰器模式?

  4. Python 装饰器 vs 装饰器模式:你有一个类,需要在运行时决定是否添加日志功能,用 Python @decorator 还是装饰器模式?为什么?

  5. 单例代理:代理模式中,代理对象本身是否应该是单例?什么情况下是,什么情况下不是?