接口不变,只往上叠职责,它是把「若干可选工序」做成正交组合的标准答案:4 个可选功能用继承是 2⁴ = 16 个子类,用装饰者只要 4 个类,而且运行时任意拼装。本文讲清四个角色与两个关键结构关系(既实现 Component、又持有 Component)、最容易被低估的顺序语义、三条边界与几个常见坑,梳理
java.io流体系、Collections.unmodifiableList、MyBatis 的CachingExecutor等真实装饰者,最后把装饰者、代理、适配器三者的界限一次讲透。
一、问题的起点:需求是「叠加」的,继承会爆炸
先看一个几乎每个项目都会长出来的需求:报表导出。
基础功能:把数据导成 CSV
可选功能:压缩成 zip、加密、加水印、记审计日志
产品的要求是"每个客户勾选不同组合":A 客户要压缩,B 客户要压缩+加密,C 客户要加密+水印+日志……用继承来穷举会发生什么?
class CsvExporter { ... }
class CsvCompressedExporter extends CsvExporter { ... }
class CsvEncryptedExporter extends CsvExporter { ... }
class CsvCompressedEncryptedExporter extends CsvCompressedExporter { ... }
class CsvCompressedEncryptedWatermarkExporter extends ... { ... }
// 4 个可选功能 → 2⁴ = 16 个类,还不算顺序不同带来的变体
方案一和方案二的差距,一眼可见:

这就是类爆炸:类数量随功能数指数增长,而且每加一个功能(比如"加签名")就要翻倍。更糟的是这些类之间是编译期绑定的——客户半夜说"把加密换成签名",你得改代码重新发版。
换一个思路:把"导出"和"加工"拆开。导出的实现只有一份,压缩 / 加密 / 水印 / 日志各自写成一个独立的"包装层",需要哪些就一层层套上去,顺序还能调:
ReportExporter exporter = new LogDecorator(
new EncryptDecorator(
new CompressDecorator(
new CsvExporter())));
这就是装饰者模式(Decorator,也译作装饰器模式、装饰模式)。它要解决的从来不是"功能不够",而是**"功能要能自由叠加、运行时组装、且不改变原有对象"**。
二、结构:四个角色,两个关键关系
装饰者的结构是结构型模式里最"讲道理"的一个,四个角色各司其职:

- Component(抽象构件):
ReportExporter,客户端唯一认识的东西。装饰后对象必须还是它,这是整个模式的地基。 - ConcreteComponent(具体构件):
CsvExporter,真正干活的实现。 - Decorator(抽象装饰者):
ExporterDecorator,既实现 Component 又持有一个 Component——这两个关系缺一不可:- 实现 Component → 装饰后的对象类型不变,客户端无需感知;
- 持有 Component → 可以包住任意 Component,于是能一层层叠。
- ConcreteDecorator(具体装饰者):
CompressDecorator、EncryptDecorator,在转发前后加上自己的加工。
客户端的一次调用实际是一层层"洋葱式"下钻、再一层层回传加工:

要点:每一层都只认识自己的 inner,不知道外面套了几层、也不知道自己是最内层还是最外层。这就是它能自由组合的原因。
三、手写一条完整的装饰者链
下面是可直接运行的单文件版本(Java 8+):
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/** Component:客户端唯一认识的抽象 */
interface ReportExporter {
String export();
}
/** ConcreteComponent:真正干活的实现,只有一份 */
class CsvExporter implements ReportExporter {
@Override
public String export() {
return "id,name,amount\n1,张三,199.00\n2,李四,88.50";
}
}
/** Decorator:实现 Component + 持有 Component,两个关系缺一不可 */
abstract class ExporterDecorator implements ReportExporter {
protected final ReportExporter inner; // 被装饰者,类型仍是抽象
protected ExporterDecorator(ReportExporter inner) {
this.inner = inner;
}
@Override
public String export() { // 默认纯转发,子类按需加工
return inner.export();
}
}
/** ConcreteDecorator:压缩 */
class CompressDecorator extends ExporterDecorator {
CompressDecorator(ReportExporter inner) { super(inner); }
@Override
public String export() {
String raw = inner.export(); // 先让内层干活
return "[zip:" + raw.length() + "B]" + raw; // 这里用标记模拟压缩
}
}
/** ConcreteDecorator:加密 */
class EncryptDecorator extends ExporterDecorator {
EncryptDecorator(ReportExporter inner) { super(inner); }
@Override
public String export() {
String raw = inner.export();
return "[enc:" + Base64.getEncoder()
.encodeToString(raw.getBytes(StandardCharsets.UTF_8)) + "]";
}
}
/** ConcreteDecorator:审计日志(前置 + 后置都能加) */
class LogDecorator extends ExporterDecorator {
LogDecorator(ReportExporter inner) { super(inner); }
@Override
public String export() {
long t0 = System.nanoTime(); // 前置
String out = inner.export();
System.out.printf("[audit] 导出完成,耗时 %dus,长度 %d%n",
(System.nanoTime() - t0) / 1000, out.length()); // 后置
return out;
}
}
public class DecoratorDemo {
public static void main(String[] args) {
// 运行时自由组装,顺序可调,客户端只认识 ReportExporter
ReportExporter a = new LogDecorator(new CompressDecorator(new CsvExporter()));
ReportExporter b = new EncryptDecorator(new CompressDecorator(new CsvExporter()));
ReportExporter c = new CompressDecorator(new EncryptDecorator(new CsvExporter()));
System.out.println(a.export());
System.out.println(b.export());
System.out.println(c.export()); // 注意:与 b 的结果不同
}
}
三个细节值得停一下:
- 抽象装饰者的
export()是纯转发,具体装饰者才选择"前置加工、后置加工、或者干脆替换返回值"。 - 装饰者的构造器接收的是抽象类型,所以
inner既可以是CsvExporter,也可以是另一个装饰者——这正是"能叠"的技术原因。 - 上一行代码里的
b和c看起来只是换了位置,结果并不一样。这就是下一节要说的顺序语义。
四、顺序是装饰者最重要的语义
装饰者的每一层都是"对上层输入的加工、对下层输出的加工",因此嵌套顺序直接决定结果。加密和压缩谁先谁后,产出的是完全不同的东西:

实践里由此派生出三条经验:
- 顺序要在代码里显式化。同一组装饰者用不同顺序拼装,是生产事故的常见来源——把拼装逻辑收在一个工厂方法里(
ExporterFactory.of(config)),不要散落在调用点。 - 有些层对顺序敏感,有些层不敏感。日志、计时这类"旁路"装饰者通常与顺序无关;加密、压缩、签名、限流这类"改变数据形态"的装饰者必须定序。
- 可逆性要想清楚。如果装饰链是"加密→压缩",接收方就得按同一顺序逆着来(先解压再解密),这套约定要写进协议文档,而不是留在某个人的脑子里。
五、装饰者的三条边界
装饰者很容易被用歪,因为它"看起来就是包一层"。下面三条边界建议当成硬约束:
1. 不改变被装饰者的接口与语义。 装饰者必须仍然"是"Component(Liskov 替换原则)。如果包装后方法名变了、参数变了、或者返回类型被改了,那不是装饰者,是适配器(见本系列《适配器模式》)。
2. 不依赖具体类型,也不该用 instanceof 判断。 装饰层的类型是嵌套的,exporter instanceof CsvExporter 在最外层永远是 false。如果需要"识别"某层,说明设计里混进了别的需求(通常该用代理或显式的元数据),不要靠类型判断硬掰。
3. 每层只做一件事,且层数要克制。 三层以内通常清晰;超过五层,异常栈、日志和调试成本会急剧上升。如果发现需要十层装饰者,多半应该换成责任链(Chain of Responsibility)或策略组合。
另外三个容易被忽略的点:
equals/hashCode语义会变。装饰后的对象与原对象不再相等(甚至两个"内容相同"的装饰链也不相等)。把它当集合键、或做缓存命中判断时,这个问题会直接变成 bug。JDK 的Collections.unmodifiableList也是同样的道理——它是视图,不是原对象。- 装饰链上的状态要分清归属。装饰者里可以放自己的状态(比如计时用的
t0),但不要修改inner的状态,否则"每层独立"的语义就被破坏了。 - 线程安全由各层自己负责。装饰者持有的状态若在多次调用之间共享,就必须自己同步;更稳的做法是让每层保持无状态,把链做成不可变对象,每次请求按配置重新拼一条链。
六、JDK 与框架里的装饰者
装饰者是 JDK 里出现最多的结构型模式——没有之一。java.io 整个包就是一套装饰者教科书:
| 位置 | Component | 具体装饰者 | 说明 |
|---|---|---|---|
java.io 字节流 | InputStream / OutputStream | BufferedInputStream、DataInputStream、GZIPInputStream、DigestInputStream | 缓冲、类型化读写、压缩、摘要,全部靠"套" |
java.io 字符流 | Reader / Writer | BufferedReader、LineNumberReader、PrintWriter | 同上,字符侧 |
java.util.Collections | List / Set / Map | unmodifiableList、synchronizedList、checkedList | 只读、加锁、类型检查,都是装饰者(返回的是视图) |
| Servlet 规范 | HttpServletRequest | HttpServletRequestWrapper | 过滤器里改请求参数/头部的标准做法 |
| MyBatis | Executor | CachingExecutor 等装饰链 | 查询缓存、懒加载等能力靠装饰者叠加 |
| Spring WebFlux | ServerHttpRequest | ServerHttpRequestDecorator | 读取并重新包装请求体,供下游多次读取 |
最典型的一行代码,一眼就是装饰者:
// 三层装饰:文件字节流 → 字符转换 → 缓冲
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("data.csv"), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) { // readLine() 是 BufferedReader 加的
System.out.println(line);
}
}
// 集合侧同理:返回的是只读视图,不是新集合
List<String> view = Collections.unmodifiableList(rawList);
// view.add("x"); // 运行期 UnsupportedOperationException
这条链的每一层「加了什么」,可以拆开看:

顺带说一句:Collections.unmodifiableList 返回的对象不是 ArrayList 的副本,而是一个包装了原集合的装饰者——原集合变了,视图跟着变。这既是它的优点(零拷贝),也是它最常被误用的地方。
七、装饰者 vs 代理 vs 适配器
这三个模式是"包一层"三兄弟,也是面试与软考里最爱对比的一组。本系列前两篇分别站在代理、适配器视角讲过,这里把三者放到一张表里:

| 维度 | 装饰者 | 代理 | 适配器 |
|---|---|---|---|
| 接口是否变化 | 不变(必须还是 Component) | 不变 | 改变(翻译成另一方认得的形式) |
| 能否叠加多层 | 能,且不限制顺序 | 通常单层 | 通常单层(双向适配器是两个方向) |
| 客户端是否感知 | 不需要感知 | 不需要感知 | 需要依赖目标接口 |
| 核心意图 | 动态增加职责 | 控制对真实对象的访问 | 让不兼容的接口能协作 |
| 典型场景 | Java IO、只读集合视图 | Spring AOP 事务、RPC stub | InputStreamReader、JDBC Driver |
一句话记法:装饰者加能力、代理控访问、适配器改接口。 前两者接口都不变,区别在"意图"和"能不能叠"——装饰者是被设计成可以层层叠加的,而代理一般只包一层,也不会去改变原对象的职责。
八、什么时候不要用装饰者
- 组合是固定的一两种,且不会变。 直接写死一个类更简单,别为了"看起来灵活"引入装饰者。
- 需要的是"加强整体能力",而不是"叠加工序"。 比如"更好的缓存策略",那是一个策略/实现替换问题,不是装饰链。
- 装饰链超过五层。 调试与排错成本会超过收益,改用责任链或显式的管道模型(Pipeline)更合适。
- 接口不稳定。 Component 每加一个方法,所有装饰者都要跟着改。接口没稳定之前,别急着上装饰者。
- 用它掩盖下游问题。 比如"下游返回的字段不对,我在装饰者里顺手改一下"——这是把 bug 藏进链路里,迟早找不到。
再补一条边界:装饰者 ≠ 责任链,也 ≠ 策略。 装饰者每一层都会经手并加工同一个请求,最终产出仍是"一个"结果;责任链是"谁接住谁处理",可能中途终止;策略是"多选一"。需求一旦变成"选一个实现"或"谁先接住谁处理",就不该继续往上套装饰者了。
一个正面的判断标准:如果需求是"若干可选工序的任意组合",装饰者几乎总是对的;如果需求是"若干互斥的实现选一个",那是策略或工厂,不是装饰者。
九、参考资料
- GoF《设计模式:可复用面向对象软件的基础》第 4 章 Decorator(对象结构型)
- 《Head First 设计模式》第 3 章 Decorator(星巴克咖啡例子,顺序语义讲得最清楚)
- JDK 源码:
java.io.BufferedInputStream、java.io.GZIPInputStream、java.util.Collections#unmodifiableList - Servlet 规范:
javax.servlet.http.HttpServletRequestWrapper - MyBatis:
org.apache.ibatis.executor.CachingExecutor - 本系列相关篇目:《设计模式(Java)· 代理模式》《设计模式(Java)· 适配器模式》——其中都有"包一层"三兄弟的对比图
评论