mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3519 字
9 分钟
装饰器模式(Decorator Pattern)
2026-06-13

一、为什么需要装饰器模式#

你写了一个文本输出组件,用户要求:有时候加粗,有时候加边框,有时候既加粗又加边框。最直接的做法是给组件类加子类——BoldTextBorderedTextBoldBorderedText。然后又来需求:支持斜体。子类变成 ItalicTextBoldItalicTextBorderedItalicTextBoldBorderedItalicText……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 恤 + 夹克 + 大衣”专门定制一件三合一外套。

咖啡店。点一杯基础咖啡,然后加奶、加糖、加奶泡。每加一种配料,价格累加,描述也累加。你不需要在菜单上列出所有组合——拿铁、卡布奇诺、馥芮白只是常用组合的名字,本质上都是基础咖啡被不同配料装饰的结果。收银系统只需要知道每种配料的价格和描述,自动叠加就行。

俄罗斯套娃。最里面的小人偶是核心对象,外面每一层都是装饰器。每打开一层,你就看到一个更大的娃娃——装饰器层层包裹,但每一层都和内层有相同的形状(接口)。调用从最外层开始,逐层向内传递,就像从最外面一层层打开套娃。

三、核心思想#

装饰器模式的核心是透明包装:装饰器和被装饰对象实现相同的接口,对外完全透明。调用方不知道也不需要知道对象被装饰了几层——它只看到接口。

classDiagram class Component { <<interface>> +operation() string } class ConcreteComponent { +operation() string } class Decorator { <<abstract>> #component: Component +operation() string } class ConcreteDecoratorA { +operation() string +addedBehavior() } class ConcreteDecoratorB { +operation() string +addedBehavior() } Component <|.. ConcreteComponent Component <|.. Decorator Decorator <|-- ConcreteDecoratorA Decorator <|-- ConcreteDecoratorB Decorator o--> Component : wraps

类图揭示了三个关键设计决策:

  • 装饰器实现相同接口——调用方无法区分装饰前后的对象,这就是”透明”的含义
  • 装饰器持有被装饰对象的引用——通过组合而非继承扩展行为
  • 装饰器可以嵌套——装饰器包装的可以是另一个装饰器,形成链式结构

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,BufferedInputStreamDataInputStream 是装饰器:

// 装饰器链:文件流 → 缓冲 → 数据读取
InputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")
)
);

FileInputStream 只负责读文件字节。BufferedInputStream 在外面包一层,加了缓冲区,减少系统调用。DataInputStream 再包一层,加了 readInt()readUTF() 等方法。每一层只做一件事,通过组合获得所有能力。如果不需要缓冲,去掉 BufferedInputStream 就行——不影响其他层。

这个设计也有被人诟病的地方:创建一个简单的带缓冲的数据读取流,要嵌套三层构造函数。但比起为每种组合写一个子类(BufferedFileInputStreamDataFileInputStreamBufferedDataFileInputStream……),装饰器的灵活性远胜一筹。

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
@timer
def 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,返回增强后的 handler
func 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,返回增强后的 Handler
type 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)) 先查缓存再带重试地查。顺序不同,行为不同。这比继承灵活得多:你不需要 CachedRetryUserServiceRetryCachedUserService 两个子类。

类方法装饰器 @log 的本质也是包装——它把原始方法替换为一个新函数,新函数在调用原方法前后加了日志。和 Python 的 @decorator 是同一思路,只是语法和元编程机制不同。

六、生产验证#

  • Java IO —— InputStream.java InputStream 是装饰器模式的教科书实现。BufferedInputStreamDataInputStreamGZIPInputStream 等都继承自 FilterInputStream(Decorator 基类),内部持有 InputStream 引用。你可以任意组合这些流——new DataInputStream(new BufferedInputStream(new GZIPInputStream(...)))——每一层只关注自己的职责。JDK 的 IO 体系证明装饰器模式在大型类库中可以稳定运行 20 多年。

  • Python functools —— functools.py Python 标准库的 functools.wrapsfunctools.lru_cache 是函数装饰器的标准实现。lru_cache 是带状态的装饰器——它在闭包里维护 LRU 缓存,装饰后的函数和原函数签名相同,但多了缓存能力。Python 的 @ 语法让装饰器成为语言级特性,几乎所有 Python Web 框架(Flask、FastAPI、Django)都基于装饰器构建路由、权限、缓存等机制。

  • Go chi —— chi/middleware chi 路由库的中间件完全是装饰器模式。middleware.Loggermiddleware.Recoverermiddleware.Timeout 等都是 func(http.Handler) http.Handler 类型的装饰器。chi 还提供了 middleware.Chain 来组合多个中间件。这个模式已经成为 Go Web 开发的事实标准——几乎所有 HTTP 框架(echo、fiber、gin)都采用相同的中间件接口。

  • Express.js —— express Express 的中间件链是装饰器模式在 Node.js 生态的典型应用。每个中间件接收 reqresnext 三个参数,调 next() 委托给下一个中间件,不调则中断链。app.use(logger)app.use(compress())app.use(auth)——每加一行就包一层装饰器,请求处理能力逐步增强。

七、小结#

何时使用

  • 运行时动态组合特性——UI 组件的边框、滚动条、主题,每种特性一个装饰器,按需组合
  • 中间件链——HTTP 请求的日志、认证、限流、压缩,每层独立,顺序可调
  • 流式处理——Java IO 的层层包装,每层只加一种能力(缓冲、压缩、加密)
  • 函数增强——Python/TypeScript 的函数装饰器,无状态的前后增强(计时、日志、重试、缓存)

何时不用

  • 行为之间存在复杂交互——如果装饰器之间需要通信(比如缓存装饰器需要通知日志装饰器),装饰器链的独立假设就打破了。这时候考虑把逻辑整合到一个类里
  • 装饰器层数过多——10 层装饰器嵌套后,调试变成噩梦:异常栈又深又难读,执行顺序不直观。此时应该简化装饰器链,把相关职责合并
  • 接口不稳定——装饰器的前提是接口不变。如果接口频繁变更,所有装饰器都要跟着改,维护成本反而比直接改实现类更高
  • 只需要一种固定组合——如果特性组合在编译时就能确定且不会变化,直接用子类更简单

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

装饰器模式(Decorator Pattern)
https://blog.souloss.com/posts/programming/behavioral/behavioral-decorator/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时