python设计模式–装饰器模式(Decorator Pattern)

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+日志+重试”,体会多层包装和顺序,这样对装饰器模式会掌握得比较扎实。

发表评论