你有没有碰到过这种场景:一个请求发出去之前,要加 token、要加日志、要加缓存、要加重试……每加一个能力,就想多写一个子类或者一个 if 分支?
加到第四个的时候,你开始怀疑人生。
这不是你的问题。是继承这条路本身会走进死胡同。 今天聊的 Decorator 模式,就是专门解决这种"能力叠加"困境的。
一、问题:子类组合爆炸
假设你在做一个通知系统。初始需求很简单——发邮件就行。后来产品说:加个短信通知;再后来:加 Slack;再后来:加企业微信。
如果用继承,3 种额外通知方式的所有组合是多少个子类?
| 通知方式 | 组合 |
|---|---|
| 短信 | 1 |
| Slack | 1 |
| 企微 | 1 |
| 短信 + Slack | 1 |
| 短信 + 企微 | 1 |
| Slack + 企微 | 1 |
| 短信 + Slack + 企微 | 1 |
| 合计 | 7 个子类 |
3 种能力,7 个子类。4 种能力,15 个子类。N 种能力,2ᴺ - 1 个子类。
这就是经典的子类组合爆炸。
子类组合爆炸:3 种通知方式需要 7 个组合子类
继承的本质问题是:它把"组合"这件事固定在了编译期。 你必须在写代码的时候就穷举所有排列组合,而现实中需求是运行时才确定的。
二、解法:一层包一层,运行时叠加
Decorator 的核心思路极其朴素:不继承,包一层。
// 基础接口
interface Notifier {
send(msg: string): void;
}
// 基础实现
class EmailNotifier implements Notifier {
send(msg: string) {
// 发邮件
}
}
// 装饰器基类
class NotifierDecorator implements Notifier {
constructor(private wrappee: Notifier) {}
send(msg: string) {
this.wrappee.send(msg); // 委托给被包装对象
}
}
// 具体装饰器:短信
class SMSDecorator extends NotifierDecorator {
send(msg: string) {
super.send(msg); // 先执行原有逻辑
// 再发一条短信
}
}
// 具体装饰器:Slack
class SlackDecorator extends NotifierDecorator {
send(msg: string) {
super.send(msg);
// 再推一条 Slack
}
}
运行时组装:
let notifier: Notifier = new EmailNotifier();
if (user.wantsSMS) notifier = new SMSDecorator(notifier);
if (user.wantsSlack) notifier = new SlackDecorator(notifier);
notifier.send("服务器着火了!");
// → 邮件 + 短信 + Slack,三条全发
7 个子类变成了 3 个装饰器类 + 1 个基础类。需要新增企微通知?加一个 WeChatDecorator,已有代码一行不改。
各种通知方式变为独立装饰器,运行时任意组合
从指数级降到线性级——这就是 Decorator 真正解决的问题。
三、为什么接口必须一致?
你可能注意到了:装饰器和被装饰对象实现同一个接口。这不是凑巧,而是整个模式能运转的核心约束。
三个理由:
| 约束 | 为什么 |
|---|---|
| 透明性 | 客户端不知道、也不需要知道对象被包了几层。它只看到 Notifier 接口 |
| 可递归 | 装饰器包装饰器包装饰器,任意嵌套深度,类型始终兼容 |
| 可替换 | 任何期望 Notifier 的地方都能传入装饰后的对象,零修改 |
这里有个很好的类比:表观遗传。
DNA 序列(核心对象)不变,但通过甲基化、乙酰化等化学修饰,同一段基因可以表达出完全不同的蛋白质。修饰不改变基因本身,但改变了它的"行为输出"。而且这些修饰是可逆的——就像你可以在运行时摘掉某一层装饰器。
基因不用突变,修饰就能决定表达。对象不用继承,包装就能改变行为。
Decorator 结构图:组件接口 → 具体组件 + 基础装饰器 → 具体装饰器
四、前端里的 Decorator 无处不在
你可能觉得设计模式是后端的事。但前端代码里,Decorator 的影子比你以为的多得多:
| 前端场景 | Decorator 体现 | 被包装的"核心对象" |
|---|---|---|
| React HOC | withAuth(withTheme(App)) | 原始组件 |
| Express/Koa middleware | app.use(cors()); app.use(logger()) | 请求处理函数 |
| Axios interceptor | request.use(addToken); response.use(handleError) | 原始请求/响应 |
| Redux middleware | applyMiddleware(thunk, logger) | dispatch 函数 |
| 埋点增强 | withTracking(Button) | 基础按钮组件 |
它们的共同特征:
不修改原对象——原始组件/函数完全不知道自己被包了
保持相同接口——包装后的东西和原来"长得一样",调用方无感知
可自由组合——加几层、加什么层,运行时决定
// Axios 拦截器 = 请求的装饰器链
axios.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`; // 第一层:加 token
return config;
});
axios.interceptors.request.use((config) => {
console.log(`[${Date.now()}] ${config.method} ${config.url}`); // 第二层:日志
return config;
});
每个 interceptor.use() 就是"再包一层"。你一直在用 Decorator,只是以前可能不知道它叫这个名字。
五、Decorator vs. 其他容易混淆的模式
| 对比 | Decorator | Adapter | Proxy |
|---|---|---|---|
| 接口 | 相同或扩展 | 完全不同 | 完全相同 |
| 目的 | 叠加新行为 | 兼容不同接口 | 控制访问 |
| 递归组合 | ✅ 支持 | ❌ 不支持 | ❌ 通常不嵌套 |
| 谁控制组装 | 客户端 | 客户端 | Proxy 自身 |
一句话区分:Decorator 加能力,Adapter 转接口,Proxy 管权限。
有一个更形象的说法:Decorator 改变对象的外皮(skin),Strategy 改变对象的内核(guts)。
六、什么时候该用 Decorator
不是所有"加功能"的场景都适合 Decorator。判断清单:
| 条件 | 说明 |
|---|---|
| ✅ 能力是独立的 | 加密、压缩、日志、缓存之间互不依赖 |
| ✅ 需要运行时动态组合 | 用户偏好、配置项决定叠哪些能力 |
| ✅ 需要保持接口不变 | 调用方不应该因为"加了日志"就改代码 |
| ✅ 能力数量可能持续增长 | 以后还会加新层,不想改老代码 |
| ❌ 能力之间有强顺序依赖 | 如果 A 必须在 B 前执行且逻辑耦合,pipeline 更合适 |
| ❌ 需要访问核心对象内部状态 | Decorator 只能通过接口操作,拿不到私有字段 |
这里的经济学类比很贴切:亚当·斯密的分工理论。把一个大流程拆成多个专业化环节(加密层、压缩层、日志层),每个环节独立可替换、独立可测试、独立可复用。Decorator 就是代码世界的专业分工——组合的边际成本趋近于零。
七、动手判断:你的代码需要 Decorator 吗?
下次写代码时,如果你发现自己在做这些事,停下来想想 Decorator:
你在一个类里堆了 5 个布尔开关——enableLog、enableCache、enableRetry……每个 if 让方法越来越胖
你在用继承组合能力——LoggingCachingRetryingService extends CachingRetryingService extends RetryingService
你在重复写同样的"前置/后置"逻辑——每个 API 方法都有一段加 token 的代码
这三种情况的共同特征是:能力是正交的,但你把它们耦合在了一起。
Decorator 的解法就像毛坯房装修——房屋结构(核心组件)不动,防水层、隔音层、装饰层、智能家居层各司其职,独立施工,随时可拆可换。
继承是推倒重建,Decorator 是模块化装修。
如果你只想带走一句话,我建议记这个:
能力叠加不靠继承靠包装。类的数量从 2ᴺ 降到 N,靠的就是把"组合"从编译期搬到运行时。
Decorator 不是什么高深的模式。它的精髓就一个字——包。包一层加一个能力,包几层运行时决定。接口不变,行为叠加,调用方无感知。
下次你写 app.use(middleware) 或者 withAuth(Component) 的时候,可以微微一笑——你已经是 Decorator 的老用户了。
参考原文:
• Refactoring.Guru — Decorator