一、为什么需要装饰器模式
你写了一个文本输出组件,用户要求:有时候加粗,有时候加边框,有时候既加粗又加边框。最直接的做法是给组件类加子类——BoldText、BorderedText、BoldBorderedText。然后又来需求:支持斜体。子类变成 ItalicText、BoldItalicText、BorderedItalicText、BoldBorderedItalicText……3 个特性已经产生 2^3 = 8 个子类。再加一个下划线特性?16 个子类。这就是子类爆炸(Subclass Explosion)。
来看膨胀的继承树:
// 继承方式:每个特性组合都需要一个子类class Text { String render() { return "文本"; } }class BoldText extends Text { String render() { return "**文本**"; } }class ItalicText extends Text { String render() { return "_文本_"; } }class BorderedText extends Text { String render() { return "| 文本 |"; } }// 3 个特性 = 7 个子类(不含基类)// 加第 4 个特性 → 15 个子类,加第 5 个 → 31 个// 组合数 = 2^N - 1继承的根本问题在于:它是编译时的静态关系。你在写代码的那一刻就锁死了特性组合,运行时无法改变。但现实需求恰恰相反——用户要在运行时决定加不加粗、加不加边框,甚至先加粗再动态地加边框。
更深层的矛盾是开闭原则(Open/Closed Principle):对扩展开放,对修改关闭。每次加新特性都要改继承树,这就违反了”修改关闭”。我们需要一种方式,在不改动已有类的前提下,动态地给它加上新行为。
装饰器模式正是为此而生:不通过继承扩展功能,而是通过包装(Wrapping)。每个装饰器持有一个被装饰对象的引用,实现与被装饰对象相同的接口,在委托调用前后添加自己的逻辑。N 个特性只需要 N 个装饰器类,运行时任意组合——2^N 种效果,但只有 N + 1 个类。
二、现实类比
穿衣。你出门前先穿 T 恤,外面套夹克,再披一件大衣。每穿一层,你的外观就多一个特征,但你还是你——T 恤、夹克、大衣都是装饰器,装饰的是你这个核心对象。脱掉大衣,你还是穿着夹克的你。每件衣服独立存在,可以随意搭配,不需要为”T 恤 + 夹克 + 大衣”专门定制一件三合一外套。
咖啡店。点一杯基础咖啡,然后加奶、加糖、加奶泡。每加一种配料,价格累加,描述也累加。你不需要在菜单上列出所有组合——拿铁、卡布奇诺、馥芮白只是常用组合的名字,本质上都是基础咖啡被不同配料装饰的结果。收银系统只需要知道每种配料的价格和描述,自动叠加就行。
俄罗斯套娃。最里面的小人偶是核心对象,外面每一层都是装饰器。每打开一层,你就看到一个更大的娃娃——装饰器层层包裹,但每一层都和内层有相同的形状(接口)。调用从最外层开始,逐层向内传递,就像从最外面一层层打开套娃。
三、核心思想
装饰器模式的核心是透明包装:装饰器和被装饰对象实现相同的接口,对外完全透明。调用方不知道也不需要知道对象被装饰了几层——它只看到接口。
类图揭示了三个关键设计决策:
- 装饰器实现相同接口——调用方无法区分装饰前后的对象,这就是”透明”的含义
- 装饰器持有被装饰对象的引用——通过组合而非继承扩展行为
- 装饰器可以嵌套——装饰器包装的可以是另一个装饰器,形成链式结构
3.1 调用链的传播
当调用被多层装饰器包装的对象时,调用从最外层开始,逐层向内传递,每一层在委托前后添加自己的行为:
// 三层装饰:压缩 → 加密 → 日志 → 核心操作Component c = new LogDecorator( new EncryptDecorator( new CompressDecorator( new ConcreteComponent() ) ));c.operation();执行过程像剥洋葱:LogDecorator.operation() 先记日志,调 EncryptDecorator.operation(),加密后再调 CompressDecorator.operation(),压缩后调 ConcreteComponent.operation()。核心操作完成后,控制权逐层返回——解压缩、解密、记完成日志。每一层只关心自己的前处理和后处理,不知道外面还包了几层。
3.2 经典实例:Java IO 流
Java IO 是装饰器模式最经典的工业应用。InputStream 是 Component 接口,FileInputStream 是 ConcreteComponent,BufferedInputStream 和 DataInputStream 是装饰器:
// 装饰器链:文件流 → 缓冲 → 数据读取InputStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("data.bin") ));FileInputStream 只负责读文件字节。BufferedInputStream 在外面包一层,加了缓冲区,减少系统调用。DataInputStream 再包一层,加了 readInt()、readUTF() 等方法。每一层只做一件事,通过组合获得所有能力。如果不需要缓冲,去掉 BufferedInputStream 就行——不影响其他层。
这个设计也有被人诟病的地方:创建一个简单的带缓冲的数据读取流,要嵌套三层构造函数。但比起为每种组合写一个子类(BufferedFileInputStream、DataFileInputStream、BufferedDataFileInputStream……),装饰器的灵活性远胜一筹。
3.3 Python functools.wraps:函数装饰器
Python 的 @decorator 语法是装饰器模式在函数级别的应用。functools.wraps 本身是一个装饰器,用来修复装饰器带来的元信息丢失问题:
import functools
def timer(func): """计时装饰器:测量函数执行时间""" @functools.wraps(func) # 保留原函数的 __name__、__doc__ 等元信息 def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) # 委托给被装饰函数 elapsed = time.perf_counter() - start print(f"{func.__name__} 耗时 {elapsed:.4f}s") return result return wrapper
@timerdef fetch_data(url): """从远程获取数据""" # ...@timer 等价于 fetch_data = timer(fetch_data)。timer 返回的 wrapper 函数和原函数有相同的调用签名,但多了计时逻辑。functools.wraps 把原函数的 __name__、__doc__、__module__ 等属性复制到 wrapper 上——没有它,所有被装饰函数的名字都会变成 wrapper,调试和文档生成都会出问题。
3.4 Go HTTP 中间件:请求级装饰器
Go 的 HTTP 中间件是装饰器模式在请求处理链上的应用。http.Handler 是 Component 接口,中间件是 Decorator——它接收一个 http.Handler,返回一个包装后的 http.Handler:
// 中间件就是装饰器:接收 handler,返回增强后的 handlerfunc loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) // 委托给下一层 log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) })}中间件链的执行过程和 Java IO 流完全一致:请求从最外层中间件进入,逐层向内传递,核心 handler 处理后,控制权逐层返回。日志中间件记开始时间、调下一层、记结束时间——标准的 before/after 模式。
四、变体与对比
| 模式 | 与装饰器的关系 | 核心区别 |
|---|---|---|
| 代理 | 结构相同——都持有目标对象引用并实现相同接口 | 装饰器增强行为,代理控制访问 |
| 适配器 | 都是对对象的包装 | 装饰器保持接口不变,适配器改变接口 |
| 责任链 | 都可以形成链式调用 | 装饰器始终委托,责任链可以中断传播 |
| 子类化 | 都是为了扩展行为 | 装饰器是运行时组合,子类化是编译时继承 |
4.1 装饰器 vs 代理:同结构,不同意图
装饰器和代理的代码结构几乎一模一样——都持有一个目标对象的引用,都实现相同的接口,都在委托前后做事情。区别在于意图:装饰器的目的是给对象加新行为,代理的目的是控制对对象的访问。
// 装饰器:加日志(增强行为)func logDecorator(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("请求开始") h.ServeHTTP(w, r) // 始终委托 log.Println("请求结束") })}
// 代理:限流(控制访问)func rateLimitProxy(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if limiter.Allow() { h.ServeHTTP(w, r) // 满足条件才委托 } else { http.Error(w, "too many requests", 429) // 不委托,直接拒绝 } })}装饰器不会阻止调用传递到内层,代理可以。实际项目中两者的边界有时模糊——一个带缓存的代理,命中时直接返回不走内层,这算代理还是装饰器?判断标准:如果包装层的核心目的是”控制是否以及何时访问目标对象”,它是代理;如果核心目的是”给目标对象增加额外行为”,它是装饰器。
4.2 装饰器 vs 适配器:保持接口 vs 改变接口
装饰器和适配器都是包装模式,但装饰器保持接口不变,适配器把一个接口转换成另一个接口。
// 装饰器:增强原有接口function withCache<T extends (...args: any[]) => any>(fn: T): T { const cache = new Map(); return ((...args: any[]) => { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; }) as T;}
// 适配器:转换接口class OldPrinter { printText(text: string): void { /* 旧接口 */ }}
class PrinterAdapter implements NewPrinter { constructor(private old: OldPrinter) {} print(doc: Document): void { this.old.printText(doc.toMarkdown()); // 新接口 → 旧接口 }}4.3 装饰器 vs 责任链:始终委托 vs 可以中断
装饰器链中每一层始终会调下一层——这是装饰器的契约。责任链中某个处理者可以决定不再传递,直接返回结果。
// 装饰器:始终委托func authDecorator(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 即使认证失败,也可以选择继续(比如降级处理) r.Header.Set("X-Auth-Status", "verified") next.ServeHTTP(w, r) // 始终调下一层 })}
// 责任链:可以中断func authHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !isValidToken(r.Header.Get("Authorization")) { http.Error(w, "unauthorized", 401) return // 中断!不调下一层 } next.ServeHTTP(w, r) })}实际项目里中间件往往兼有两种特征:认证中间件在 token 无效时中断请求,日志中间件始终委托。严格来说,中断传播的中间件更像责任链节点,始终委托的更像装饰器。但没必要纠结分类——重要的是理解两种行为的区别。
4.4 函数装饰器 vs 类装饰器
GoF 原著描述的是类级装饰器——装饰器是一个类,包装另一个类。Python 的 @decorator 和 TypeScript 的装饰器语法是函数级装饰器——装饰器是一个函数,包装另一个函数。两者的核心思想一致,但应用层次不同。
类装饰器适合需要维护状态的场景(比如 BufferedInputStream 内部维护缓冲区),函数装饰器适合无状态的前后增强(比如计时、日志、权限检查)。现代语言越来越倾向于函数装饰器——更轻量,组合更方便,不需要为每种装饰定义一个类。
五、多语言实现
5.1 Go 实现:HTTP 中间件链
// Handler 是被装饰的接口(对应 Component)type Handler interface { ServeHTTP(w http.ResponseWriter, r *http.Request)}
// Middleware 是装饰器工厂:接收 Handler,返回增强后的 Handlertype Middleware func(http.Handler) http.Handler
// Chain 将多个中间件组合成一条装饰器链func Chain(middlewares ...Middleware) Middleware { return func(final http.Handler) http.Handler { // 从后往前包装:最后一个中间件最先包装核心 handler for i := len(middlewares) - 1; i >= 0; i-- { final = middlewares[i](final) } return final }}
// 日志装饰器func Logging() Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) // 委托给下一层 log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) }}
// 计时装饰器func Timing() Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) elapsed := time.Since(start) r.Header.Set("X-Response-Time", elapsed.String()) }) }}
// 恢复装饰器:捕获 panic,防止整个服务崩溃func Recovery() Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf("panic 已恢复: %v", err) http.Error(w, "内部错误", 500) } }() next.ServeHTTP(w, r) }) }}
// 使用:三层装饰器链handler := Chain( Recovery(), // 最外层:捕获 panic Logging(), // 中间层:记录日志 Timing(), // 最内层:测量耗时)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("你好,世界"))}))Chain 函数是关键——它把多个独立的中间件串联成一条链。从后往前包装保证执行顺序和声明顺序一致:Recovery 最先执行(最外层),Timing 最后执行(最内层,紧贴核心 handler)。这和 Java IO 的嵌套构造是同一模式,只是用函数组合替代了构造函数嵌套。
Recovery 中间件展示了装饰器的一个实用模式:后处理(after behavior)。defer 确保无论 next.ServeHTTP 是否 panic,恢复逻辑都会执行。这比在每个 handler 里写 defer recover 干净得多。
5.2 TypeScript 实现:函数装饰器与类方法装饰器
// 函数装饰器:缓存function withCache<T extends (...args: any[]) => any>(fn: T): T { const cache = new Map<string, ReturnType<T>>(); return ((...args: any[]) => { const key = JSON.stringify(args); if (cache.has(key)) { console.log(`缓存命中: ${fn.name}`); return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }) as T;}
// 函数装饰器:重试function withRetry<T extends (...args: any[]) => any>( fn: T, maxRetries = 3, delay = 1000): T { return (async (...args: any[]) => { for (let i = 0; i <= maxRetries; i++) { try { return await fn(...args); } catch (err) { if (i === maxRetries) throw err; console.log(`${fn.name} 第 ${i + 1} 次重试...`); await new Promise((r) => setTimeout(r, delay * (i + 1))); } } }) as T;}
// 组合装饰器:缓存 + 重试,顺序决定行为const fetchWithRetryAndCache = withCache(withRetry(fetchData, 3));// 先查缓存,未命中则带重试地请求
// 类方法装饰器(Stage 3 装饰器提案)function log( target: any, context: ClassMethodDecoratorContext) { return function (this: any, ...args: any[]) { console.log(`调用 ${String(context.name)},参数:`, args); const result = target.apply(this, args); console.log(`${String(context.name)} 返回:`, result); return result; };}
class UserService { @log getUser(id: string) { return db.findUser(id); }}函数装饰器可以自由组合——withCache(withRetry(fn)) 先加重试再缓存结果,withRetry(withCache(fn)) 先查缓存再带重试地查。顺序不同,行为不同。这比继承灵活得多:你不需要 CachedRetryUserService 和 RetryCachedUserService 两个子类。
类方法装饰器 @log 的本质也是包装——它把原始方法替换为一个新函数,新函数在调用原方法前后加了日志。和 Python 的 @decorator 是同一思路,只是语法和元编程机制不同。
六、生产验证
-
Java IO —— InputStream.java
InputStream是装饰器模式的教科书实现。BufferedInputStream、DataInputStream、GZIPInputStream等都继承自FilterInputStream(Decorator 基类),内部持有InputStream引用。你可以任意组合这些流——new DataInputStream(new BufferedInputStream(new GZIPInputStream(...)))——每一层只关注自己的职责。JDK 的 IO 体系证明装饰器模式在大型类库中可以稳定运行 20 多年。 -
Python functools —— functools.py Python 标准库的
functools.wraps和functools.lru_cache是函数装饰器的标准实现。lru_cache是带状态的装饰器——它在闭包里维护 LRU 缓存,装饰后的函数和原函数签名相同,但多了缓存能力。Python 的@语法让装饰器成为语言级特性,几乎所有 Python Web 框架(Flask、FastAPI、Django)都基于装饰器构建路由、权限、缓存等机制。 -
Go chi —— chi/middleware chi 路由库的中间件完全是装饰器模式。
middleware.Logger、middleware.Recoverer、middleware.Timeout等都是func(http.Handler) http.Handler类型的装饰器。chi 还提供了middleware.Chain来组合多个中间件。这个模式已经成为 Go Web 开发的事实标准——几乎所有 HTTP 框架(echo、fiber、gin)都采用相同的中间件接口。 -
Express.js —— express Express 的中间件链是装饰器模式在 Node.js 生态的典型应用。每个中间件接收
req、res、next三个参数,调next()委托给下一个中间件,不调则中断链。app.use(logger)、app.use(compress())、app.use(auth)——每加一行就包一层装饰器,请求处理能力逐步增强。
七、小结
何时使用:
- 运行时动态组合特性——UI 组件的边框、滚动条、主题,每种特性一个装饰器,按需组合
- 中间件链——HTTP 请求的日志、认证、限流、压缩,每层独立,顺序可调
- 流式处理——Java IO 的层层包装,每层只加一种能力(缓冲、压缩、加密)
- 函数增强——Python/TypeScript 的函数装饰器,无状态的前后增强(计时、日志、重试、缓存)
何时不用:
- 行为之间存在复杂交互——如果装饰器之间需要通信(比如缓存装饰器需要通知日志装饰器),装饰器链的独立假设就打破了。这时候考虑把逻辑整合到一个类里
- 装饰器层数过多——10 层装饰器嵌套后,调试变成噩梦:异常栈又深又难读,执行顺序不直观。此时应该简化装饰器链,把相关职责合并
- 接口不稳定——装饰器的前提是接口不变。如果接口频繁变更,所有装饰器都要跟着改,维护成本反而比直接改实现类更高
- 只需要一种固定组合——如果特性组合在编译时就能确定且不会变化,直接用子类更简单
八、参考资料
- Design Patterns: Elements of Reusable Object-Oriented Software - GoF 原著,装饰器模式的经典定义
- Python functools 文档 - Python 标准库中装饰器工具的官方文档
- chi middleware 源码 - Go HTTP 中间件装饰器的工业级实现
- Express 中间件指南 - Node.js 中间件链的官方文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






