装饰者模式这个名字,听着像某种装修手法,其实它是面向对象设计模式里最贴近日常场景的一个。我第一次对它有感觉,不是在读设计模式的书,而是在改一段全是 if-else 的下单结算代码时被逼出来的。那会儿需求是这样的:订单要能叠加会员折扣、满减券、加急费,还要考虑不同配送方式的附加成本,每个需求进来我就在原来的方法里再塞一个分支,几个月下来,那段代码连我自己看着都头疼。后来我意识到,这种“往一个类里不断追加职责”的做法迟早要崩,于是开始把每个变动拆成独立的装饰层,像搭积木一样动态组合,问题一下清晰了。
这篇博文就围绕装饰者模式来展开:它到底解决什么问题、核心机制是什么、真实项目里哪些地方早就用了它、和代理模式组合模式怎么区分,以及我实际踩过的坑和一直用着的判断标准。无论你是刚接触设计模式的新手,还是写了好几年业务代码想优化结构的开发者,这篇应该都能给你一些可以直接上手的参考。
1. 从一杯咖啡说起:继承扩展的边界到底在哪
1.1 一个小需求如何演变成子类爆炸
先看一个经典例子。假设你开了一家咖啡店,饮品种类不多,就两种:浓缩咖啡(Espresso)和美式咖啡(House Blend Coffee)。价格也简单,浓缩12块,美式10块。用面向对象来表达,很自然就是两个类,各自实现一个cost()方法。
但顾客不可能只喝纯咖啡,他们会说“帮我加一份奶”、“加一份摩卡”、“两份奶不要糖”。调料种类一多,问题就来了。如果按继承的思路,每一种“饮品+调料”的组合都得建一个类:
- Espresso
- EspressoWithMilk
- EspressoWithMocha
- EspressoWithMilkAndMocha
- HouseBlendWithMilk
- HouseBlendWithMocha
- HouseBlendWithMilkAndMocha
- …
这还只是2种咖啡、2种调料的情况,总共就要8个类。如果咖啡种类变成5种,调料变成8种,那总组合数是 5 × 2^8 = 1280 个类。这不是夸张,这是纯粹的数学问题——每加一种调料,组合数量就翻倍,子类数量直接指数爆炸。
我见过不少项目的早期代码就是这样:一开始只有两三个变体,大家图省事直接继承,等到变体超过十个,类的命名就开始混乱,什么SpecialOrderWithDiscountAndExpressAndGiftPack这种名字都出来了。更崩溃的是,如果某一天要调整所有含奶制品的饮品价格,你得把所有相关的子类都翻出来改一遍,漏掉一个就是线上事故。
1.2 继承的第二个硬伤:编译期就锁死了能力
子类爆炸还不是继承最隐蔽的问题。继承的扩展能力,在编译期就把对象的能力锁死了。什么意思?就是说你声明一个变量是Espresso类型,那它在运行时就只能是Espresso本身或者它的子类,它能做什么、不能做什么,在你写下那行代码的时候就已经决定好了。
但是现实的业务需求往往不是这样。一个订单是不是要加急、要不要用优惠券、配送方式是什么,这些常常是运行时根据用户选择、库存情况、甚至系统当前负载动态决定的。用继承去表达这种“运行中还不确定”的职责组合,会非常别扭:你可能得在代码里预置大量的子类去覆盖各种组合,可你根本不知道用户到底会选哪种组合。
还有一个容易被忽略的问题:深继承链的脆弱性。当继承层次超过三层,父类的任何一个方法改动都可能波及所有子类,而子类为了满足特定需求又不得不覆写父类方法,结果就是“脆弱的基类问题”。我见过一个案例,基类里加了一个默认返回false的isFreeShipping()方法,结果底下某个子类忘了覆写,用户莫名被收了运费,排查了很久才发现是继承链上某个中间层改了行为。
继承不是不好,它是静态的、单维度的扩展方式,适合“同一类东西的纵向细化”,但不适合“多个维度的横向组合”。当你要扩展的不只是“一种咖啡”,而是“咖啡 × 调料 × 杯型 × 温度 × 糖度”这种多维组合时,就得换一种思路——不是“我是什么”,而是“我包着什么”。这就是装饰者模式登场的时机。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 装饰者模式的骨架:组合转发把功能一层层叠上去
2.1 四个角色的分工
装饰者模式的结构其实很直白,就四个角色:
| 角色 | 别名 | 职责 |
|---|---|---|
| 组件接口 | Component | 定义业务方法的统一接口,让装饰者和被装饰者能互相替换 |
| 具体组件 | ConcreteComponent | 真正干活的原始对象,是被装饰的对象 |
| 抽象装饰者 | Decorator | 持有组件接口的引用,转发所有方法调用 |
| 具体装饰者 | ConcreteDecorator | 在转发调用的前后附加自己的职责 |
这里最核心的一个设计决策是:抽象装饰者本身也实现了同一个组件接口。正是这一条让装饰者的“套娃”成为可能——每一个装饰者包装出来的对象,类型仍然是组件接口,可以被下一个装饰者继续包装,也可以直接当作原始对象使用。
很多初学者理解不了为什么要多此一举搞一个抽象装饰者。我的理解是:它解决的是“统一”的问题。如果没有抽象装饰者,每个具体装饰者都得自己持有组件引用、自己实现接口方法,代码大量重复且容易漏实现。有了抽象装饰者,公共的“持有被装饰对象”的逻辑被收拢到一处,具体装饰者只需要专注写自己的那点增强逻辑。
另外,这个角色还暗示了一个约束:装饰者模式要求组件接口尽量简单、稳定。如果你的组件接口有二十个方法,每个装饰者都要转发二十个方法,这个模式的成本就很高。我后面会专门讲这个问题。
2.2 一段能跑的代码:咖啡加奶加摩卡
理论说再多,不如看一段能跑的代码。我用Java来写,因为Java在类型表达上最严谨,能把这个结构讲透。
先定义组件接口和具体组件:
java复制// 组件接口:饮品
public interface Beverage {
String getDescription(
