Python 设计模式——装饰器模式(Decorator Pattern)详解
本文面向零基础新手,从概念到“包装”与“同一接口”,从简单示例到多层装饰,全方位讲解装饰器模式。
一、什么是装饰器模式?
1.1 通俗理解
装饰器模式是一种结构型设计模式,核心思想是:
在不修改原类、不通过继承增加子类的前提下,给对象“包一层”(装饰器),这层对外保持和原对象相同的接口,但在调用时可以在前后增加自己的逻辑(如日志、校验、增强结果),从而动态地给对象增加职责。
可以这样类比:
- 现实中的装饰:一杯咖啡是“基础”;加奶、加糖、加杯套,都是在“外面包一层”,你拿到的还是“一杯饮料”,但味道和体验变了。每层包装都不改变“喝”这个接口,只是在喝之前/之后多了奶、糖等。
- 代码里:有一个“发消息”的对象;你用一个“带日志的发消息”包装它:对外还是“发消息”,但每次发之前先打日志,再转给里面的对象去发。包装层(装饰器)和被包装对象(被装饰者)实现同一接口,装饰器内部持有被装饰者,在调用前后加逻辑。
所以,装饰器模式解决的是:想给对象动态加功能,又不想改原类、不想用继承爆出一堆子类(如 Coffee、MilkCoffee、SugarCoffee、MilkSugarCoffee…),就用“包装”一层层加。
1.2 为什么需要装饰器模式?
场景一:职责要动态叠加
同一类对象,有时要“只发消息”,有时要“带日志的发消息”,有时要“带重试的发消息”,有时要“带日志且带重试”。若用继承,会组合爆炸(LogSender、RetrySender、LogRetrySender…)。用装饰器:先有一个普通 Sender,外面包一层 LogDecorator 得到“带日志的”,再在外面包一层 RetryDecorator 得到“带日志且带重试的”,职责可任意组合、顺序可调。
场景二:不能改原类
原类是第三方库或遗留代码,不能改源码;又希望在所有调用前后加逻辑(如日志、计时、权限),就可以写一个装饰器类包装原类实例,在相同接口方法里先做自己的事,再调用被包装对象。
场景三:可选的增强
同一接口的多种实现(如多种 Writer),有的要缓冲、有的要压缩、有的要加密,可以分别用 BufferedDecorator、CompressDecorator、EncryptDecorator 包装,按需包装,不写 N×M 个子类。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 动态、可组合地增加职责 | 日志、重试、缓存、校验等,可任意叠加、顺序可调。 |
| 不能修改原类 | 用包装加行为,不改源码。 |
| 替代大量子类 | 避免为每种组合写一个子类(如 MilkSugarCoffee、MilkCoffee…)。 |
简单记忆:要“在原有行为前后加一层逻辑”、且希望可叠加、可插拔时,用装饰器模式。
二、装饰器模式的结构(四个角色)
| 角色 | 说明 |
|---|---|
| 组件(Component) | 抽象接口,定义“被装饰者”和“装饰器”共有的方法(如 send、cost)。 |
| 具体组件(ConcreteComponent) | 被装饰的原始对象,实现组件接口。 |
| 装饰器(Decorator) | 实现组件接口,并持有一个组件(被装饰者);在实现接口时先/后做自己的事,再调用被装饰者的同一方法。 |
| 具体装饰器(ConcreteDecorator) | 具体的“加一层”逻辑,如加日志、加奶、加重试。 |
关系可以理解为:
- 客户拿到的可能是“裸组件”,也可能是“被一层或多层装饰器包住的组件”;客户只调接口,不关心有几层。
- 装饰器和具体组件都实现同一接口;装饰器内部持有一个组件,调用时先做自己的逻辑(或后做),再
self._component.xxx()转给被装饰者。 - 可以多层包装:DecoratorA(DecoratorB(ConcreteComponent())),形成一条链,请求从外到里传,响应从里到外回。
三、示例一:咖啡加料(最经典)
需求:咖啡有基础价格;可以加奶、加糖,每加一种就多一份钱。用装饰器:基础咖啡是具体组件,奶、糖是具体装饰器,都实现“价格”接口。
3.1 组件接口 + 具体组件
from abc import ABC, abstractmethod
class Beverage(ABC):
"""组件:饮料"""
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def desc(self) -> str:
pass
class Coffee(Beverage):
"""具体组件:纯咖啡"""
def cost(self) -> float:
return 10.0
def desc(self) -> str:
return "咖啡"
3.2 装饰器基类 + 具体装饰器
class BeverageDecorator(Beverage):
"""装饰器:持有一个饮料,对外仍是饮料"""
def __init__(self, beverage: Beverage):
self._beverage = beverage
def cost(self) -> float:
return self._beverage.cost() # 子类可在此基础上加价
def desc(self) -> str:
return self._beverage.desc()
class MilkDecorator(BeverageDecorator):
def cost(self) -> float:
return self._beverage.cost() + 2.0
def desc(self) -> str:
return self._beverage.desc() + "+奶"
class SugarDecorator(BeverageDecorator):
def cost(self) -> float:
return self._beverage.cost() + 1.0
def desc(self) -> str:
return self._beverage.desc() + "+糖"
3.3 使用:任意组合、多层包装
b1 = Coffee()
print(b1.desc(), b1.cost()) # 咖啡 10.0
b2 = MilkDecorator(Coffee())
print(b2.desc(), b2.cost()) # 咖啡+奶 12.0
b3 = SugarDecorator(MilkDecorator(Coffee()))
print(b3.desc(), b3.cost()) # 咖啡+奶+糖 13.0
要点:客户只依赖 Beverage;加料顺序不同就换包装顺序,无需为“咖啡+奶+糖”单独写一个类。
四、示例二:发消息 + 日志与重试装饰器
需求:有一个“发消息”的接口;要在不修改原实现的前提下,增加“发前打日志”和“失败重试”。用装饰器包装。
4.1 组件接口 + 具体组件
class Sender(ABC):
@abstractmethod
def send(self, msg: str) -> bool:
pass
class SimpleSender(Sender):
def send(self, msg: str) -> bool:
print(f" [SimpleSender] 发送: {msg}")
return True
4.2 装饰器:日志、重试
class SenderDecorator(Sender):
def __init__(self, sender: Sender):
self._sender = sender
def send(self, msg: str) -> bool:
return self._sender.send(msg)
class LogSenderDecorator(SenderDecorator):
def send(self, msg: str) -> bool:
print("[Log] 准备发送:", msg)
result = self._sender.send(msg)
print("[Log] 发送结果:", result)
return result
class RetrySenderDecorator(SenderDecorator):
def __init__(self, sender: Sender, max_retry: int = 2):
super().__init__(sender)
self._max_retry = max_retry
def send(self, msg: str) -> bool:
for i in range(self._max_retry + 1):
if self._sender.send(msg):
return True
print(f" 重试 {i+1}/{self._max_retry}")
return False
4.3 使用
s = LogSenderDecorator(SimpleSender())
s.send("hello") # 先打日志,再真正发
s2 = RetrySenderDecorator(LogSenderDecorator(SimpleSender()))
s2.send("hi") # 先重试层,再日志层,再真正发
要点:同一接口 send,装饰器在调用前后加日志、重试;组合顺序可调(先日志再重试,或先重试再日志),无需改 SimpleSender。
五、示例三:带“状态”的装饰器(如缓存)
装饰器不仅可以“前后加逻辑”,还可以改变行为:例如第一次调用真正执行并缓存结果,后续调用直接返回缓存。
class CachedSenderDecorator(SenderDecorator):
def __init__(self, sender: Sender):
super().__init__(sender)
self._cache = {}
def send(self, msg: str) -> bool:
if msg in self._cache:
print("[Cache] 命中:", msg)
return self._cache[msg]
result = self._sender.send(msg)
self._cache[msg] = result
return result
客户拿到的是“带缓存的发送器”,接口仍是 send(msg),但内部多了缓存逻辑。
六、Python 的 @decorator 与“类装饰器模式”的区别
- 函数装饰器
@decorator:在 Python 里,@decorator是用来包装函数的语法糖,装饰器函数接收一个函数,返回一个新函数(或可调用对象)。这和设计模式里的“装饰器模式”思想一致(不改原函数、外面包一层),但作用对象是函数,不是“组件对象”。 - 类装饰器模式:上面咖啡、Sender 的例子是对象层面的装饰器模式:组件是对象,装饰器也是对象,包装的是对象,接口是类的方法(如 cost、send)。
两者可以结合:例如用类实现一个“带日志的 Sender”,既符合装饰器模式,也可以在内部用@wraps等包装方法;或用@decorator包装一个类,让该类变成单例等。
本文重点是对象层面的装饰器模式(包装对象、同一接口、可多层叠加)。
七、装饰器模式 vs 其他模式
| 模式 | 目的 | 与装饰器的区别 |
|---|---|---|
| 装饰器 | 动态给对象加职责,同一接口,可多层包装 | 强调“包装”“同一接口”“可叠加”。 |
| 适配器 | 接口不兼容时做转译 | 改变接口;装饰器保持接口不变、只加行为。 |
| 桥接 | 抽象与实现分离,两维独立变化 | 侧重两维组合,不是“一层层包”。 |
| 组合 | 树形结构,叶子和容器统一接口 | 是“部分-整体”树;装饰器是“链式包装”,通常不是树。 |
简单记忆:装饰器 = 同一接口 + 包装 + 前后加逻辑,不改变被装饰者的接口。
八、常见问题与注意点
8.1 装饰顺序
多层装饰时,最外层先被调用。例如 LogDecorator(RetryDecorator(Sender())),发消息时先走 Log(打日志),再走 Retry(重试),最后才到 Sender。若希望“先重试再打日志”,就换成 RetryDecorator(LogDecorator(Sender()))。
8.2 装饰器要不要继承“装饰器基类”
不一定。只要装饰器类实现组件接口并持有一个组件,在方法里先/后做自己的事再调用被装饰者即可。用“装饰器基类”可以少写重复的“持有组件并转发”的代码,便于扩展具体装饰器。
8.3 不要滥用
若只有一种固定增强、且不会和别的增强组合,直接继承或在一个类里写死也可以;装饰器适合多种职责可动态组合、可插拔的场景。
九、小结
- 装饰器模式:用装饰器对象包装被装饰对象,装饰器与被装饰者实现同一接口,装饰器在调用时先/后执行自己的逻辑再转给被装饰者,从而动态、可组合地增加职责,无需改原类、无需子类爆炸。
- 四个角色:组件(接口)、具体组件、装饰器(持有一个组件)、具体装饰器。
- 典型用途:咖啡加料、带日志/重试/缓存的消息发送、流式 IO 的增强(缓冲、压缩)等。
- 实现要点:装饰器实现组件接口;内部持有组件;在接口方法里先做自己的事(或后做),再调用
self._component.xxx()。
建议先写“咖啡+奶+糖”一例,再写“Sender+日志+重试”,体会多层包装和顺序,这样对装饰器模式会掌握得比较扎实。