走进 Gang of Four 设计模式:装饰器模式
一、概念:什么是装饰器模式?
装饰器模式(Decorator Pattern)是一种结构型设计模式 ,它允许你在不改变原有对象结构的前提下,动态地向对象添加新功能。
通俗地讲:你把一个对象当作"素体",然后用一个或多个"装饰器"把它一层层包裹起来。每一层包裹都可以在原功能的基础上增加新的行为,而最外层的包装器和最里层的素体对外暴露的是同一套接口------调用方根本感知不到自己被"装饰"了。
官方定义: 动态地给一个对象添加额外的职责,同时不改变其结构。装饰器模式提供了一种灵活的替代继承方式来扩展功能。
二、动机:为什么要用装饰器模式?
2.1 它解决了什么问题?
在面向对象设计中,我们通常用继承 来扩展类的功能。但继承有一个致命缺陷:它在编译期就把功能组合写死了。
举个例子:你有一个"窗口"类,想给它加边框、加滚动条、加阴影。如果用继承,你需要写:
WindowWithBorderWindowWithScrollbarWindowWithShadowWindowWithBorderAndScrollbarWindowWithBorderAndShadowWindowWithScrollbarAndShadowWindowWithBorderAndScrollbarAndShadow
三个功能就爆出 7 个子类,四个功能就是 15 个------这就是著名的类爆炸问题。
装饰器模式用组合代替继承来解决这个困境:你把"边框""滚动条""阴影"分别做成装饰器,运行时想套几层就套几层。
2.2 适用场景
- 需要在不增加大量子类的情况下扩展类的功能
- 需要动态地添加或撤销对象的功能(运行时决定,而非编译时写死)
- 需要以一种透明、可组合的方式为对象叠加多个增强
三、核心角色与结构
装饰器模式包含 4 个核心角色,它们的职责划分非常清晰:
| 角色 | 名称 | 职责 |
|---|---|---|
| 抽象组件 | Component | 定义原始对象和装饰器的公共接口。无论包多少层,对外都是这个类型。 |
| 具体组件 | Concrete Component | 被装饰的原始对象,提供基础功能。 |
| 抽象装饰器 | Decorator | 实现 Component 接口,内部持有一个 Component 引用。默认把所有调用转发给内部对象。 |
| 具体装饰器 | Concrete Decorator | 继承抽象装饰器,在其方法调用前后添加额外功能。 |
💡 关键理解: 装饰器模式通过嵌套包装多个装饰器对象,可以实现多层次的功能增强。每个具体装饰器都可以选择性地增加新功能,同时保持对象接口的一致性------这就是"俄罗斯套娃"式的设计。
四、快速上手:给形状涂颜色
下面通过一个简单例子来演示装饰器模式的用法。我们将把一个形状装饰上不同的颜色,同时不改变形状类本身。
4.1 类图结构
┌──────────┐
│ Shape │ ← 抽象组件 (Component)
│ draw() │
└────┬─────┘
│
┌────┴────────────────┐
│ │
┌───┴──────────┐ ┌──────┴──────────────┐
│ Rectangle │ │ ShapeDecorator │ ← 抽象装饰器 (Decorator)
│ Circle │ │ - decoratedShape │ 持有 Component 引用
│ │ │ + draw() │
└──────────────┘ └──────┬──────────────┘
具体组件 │
(Concrete Component) ┌────┴────────────────┐
│ RedShapeDecorator │ ← 具体装饰器
│ + draw() │ (Concrete Decorator)
│ - setRedBorder() │
└─────────────────────┘

4.2 步骤 1:定义抽象组件(Component 接口)
java
// Shape.java
public interface Shape {
void draw();
}
4.3 步骤 2:创建具体组件(Concrete Component)
java
// Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Rectangle");
}
}
// Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Circle");
}
}
4.4 步骤 3:创建抽象装饰器(Decorator)
java
// ShapeDecorator.java
public abstract class ShapeDecorator implements Shape {
protected Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape) {
this.decoratedShape = decoratedShape;
}
public void draw() {
decoratedShape.draw(); // 默认转发
}
}
🔑 关键点: 抽象装饰器同时做了两件事------
implements Shape(对外是 Shape)和持有Shape引用(对内转发)。这就是装饰器模式的灵魂:既是 Is-A,又是 Has-A。
4.5 步骤 4:创建具体装饰器(Concrete Decorator)
java
// RedShapeDecorator.java
public class RedShapeDecorator extends ShapeDecorator {
public RedShapeDecorator(Shape decoratedShape) {
super(decoratedShape);
}
@Override
public void draw() {
decoratedShape.draw(); // 先执行原有功能
setRedBorder(decoratedShape); // 再添加新功能
}
private void setRedBorder(Shape decoratedShape) {
System.out.println("Border Color: Red");
}
}
4.6 步骤 5:组装与使用
java
// DecoratorPatternDemo.java
public class DecoratorPatternDemo {
public static void main(String[] args) {
Shape circle = new Circle(); // 素体
ShapeDecorator redCircle = new RedShapeDecorator(new Circle()); // 装饰一个圆
ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle()); // 装饰一个矩形
System.out.println("Circle with normal border");
circle.draw();
System.out.println("\nCircle of red border");
redCircle.draw();
System.out.println("\nRectangle of red border");
redRectangle.draw();
}
}
4.7 输出结果
Circle with normal border
Shape: Circle
Circle of red border
Shape: Circle
Border Color: Red
Rectangle of red border
Shape: Rectangle
Border Color: Red
✨ 观察:
redCircle和redRectangle在原有draw()行为的基础上,无侵入地 增加了"红色边框"功能。原始的Circle和Rectangle类没有任何改动。
五、深入理解:两个经典实战案例
上面的 Shape 例子帮你理解了基本结构,但装饰器模式真正的威力体现在实际工程中。下面两个案例代表了装饰器模式的两种截然不同的应用方向:
| 方向 | 案例 | 核心思路 |
|---|---|---|
| 偷梁换柱式拦截 | SpringMVC HttpHeadResponseDecorator |
拦截某些操作,改变行为 |
| 层层加码式增强 | Java I/O 流链式调用 | 叠加多个增强,逐步升级 |
5.1 案例一:SpringMVC 的 HttpHeadResponseDecorator
💡 一句话总结
拦截掉写入 Body 的操作,只放行 Header 操作,用"偷梁换柱"的方式零侵入地支持 HTTP HEAD 请求。
业务背景
假设你在开发一个视频下载网站。客户端在下载 1GB 视频之前,通常会先发一个 HEAD 请求来"试探":
- 这个文件还在吗?(看状态码是不是 200)
- 文件有多大?(看
Content-Length头) - 什么格式?(看
Content-Type头)
HEAD 请求的语义是:只要响应头,不要响应体(Body)。客户端不应该为了一次试探就去下载 1GB 的数据。
开发者的困境
你的 Controller 里通常只有一个处理下载的 GET 方法:
java
@GetMapping("/download/video")
public void downloadVideo(HttpServletResponse response) {
response.setContentType("video/mp4");
response.setContentLength(1024 * 1024 * 1024); // 1GB
// 狂写响应体------极其耗费带宽和时间
byte[] buffer = new byte[8192];
InputStream in = new FileInputStream("super_movie.mp4");
OutputStream out = response.getOutputStream();
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length); // 往网络流里写数据
}
}
你并没有写专门的 HEAD 处理方法,但客户端发来 HEAD /download/video 时,Spring 竟然瞬间返回、没有消耗 1GB 带宽。这是怎么做到的?
装饰器的魔法
当 Spring 的中央调度器(DispatcherServlet)发现这是一个 HEAD 请求,但你只有 GET 处理器时:
- 创建装饰器: 拿出原生
HttpServletResponse(真正连着网卡的那个对象),把它塞进HttpHeadResponseDecorator里。 - 欺骗 Controller: 把这个装饰器对象传给
downloadVideo方法。你的代码完全不知道传进来的是"假货"------因为它们实现了同样的接口。 - 放行 Header:
response.setContentType()→ 装饰器立刻转发给肚子里的真实对象。 - 吞噬 Body(核心!): 当代码执行到
out.write(buffer)时,装饰器提供了一个假的 OutputStream 。这个假流的write()方法什么都没做------数据倒进了黑洞,根本没发到网卡上。
角色对应
| 角色 | 对应类 | 说明 |
|---|---|---|
| 抽象组件 | HttpServletResponse |
Servlet 规范接口 |
| 具体组件 | Tomcat 的 ResponseFacade |
真正连着网卡的对象 |
| 抽象装饰器 | HttpServletResponseWrapper |
Servlet 自带的包装基类,默认全部转发 |
| 具体装饰器 | HttpHeadResponseDecorator |
Spring 的实现:拦截 Body 写入 |
关键启示
你无需为 HEAD 请求写一行额外代码。装饰器模式让框架在运行时"偷换"了响应对象,优雅地解决了这个问题。
5.2 案例二:Java I/O 流的链式调用
💡 一句话总结
像穿机甲一样层层嵌套,每一层增加一种能力:FileInputStream 当底座 → BufferedInputStream 加速 → ObjectInputStream 反序列化。
业务背景
你在开发一款单机游戏,玩家点击"继续游戏"。你需要从硬盘的 save.dat 文件中读取玩家的角色对象(Player 类,包含血量、等级、装备)。但硬盘只认识 0 和 1,不认识 Player 对象------直接读效率极低,而且你不想自己写字节解析逻辑。
层层包装的过程
java
// 步骤 1:基础组件 ------ 只能一勺一勺舀水
InputStream in = new FileInputStream("save.dat");
// 步骤 2:第一层装饰器 ------ 换成水桶舀水(批量读取)
BufferedInputStream bis = new BufferedInputStream(in);
// 步骤 3:第二层装饰器 ------ 把水变成可乐(字节 → 对象)
ObjectInputStream ois = new ObjectInputStream(bis);
// 最终使用
Player myPlayer = (Player) ois.readObject();
每层装甲的职责
-
FileInputStream(素体 / 具体组件)- 能力: 和操作系统打交道,从硬盘逐字节读取。
- 痛点: 硬盘 I/O 比内存慢成千上万倍。每次
read()相当于拿着小汤勺跑去 10 公里外的水库舀一滴水回来------卡到怀疑人生。
-
BufferedInputStream(第一层装饰器)- 能力增强: 内部维护一个 8KB 的缓冲区。
- 如何工作: 第一次读时,驱使其中的
FileInputStream开着卡车去硬盘一次性拉 8KB 回内存。之后你要数据,直接从内存缓冲区给------直到 8KB 耗尽再去拉下一车。 - 装饰器特征: 它对外还是
InputStream,但拦截了read(),加入了"批量拉取"的策略。
-
ObjectInputStream(第二层装饰器)- 能力增强: 把枯燥的二进制字节自动组装成 Java 堆内存中的真实对象。
- 如何工作: 调用
ois.readObject()时,它向下调用bis.read()索要字节,然后按 Java 序列化协议拼出Player对象。
角色对应
| 角色 | 对应类 | 说明 |
|---|---|---|
| 抽象组件 | java.io.InputStream |
定义 read() 等基本方法的抽象类 |
| 具体组件 | FileInputStream、SocketInputStream |
真正跟文件系统/网络打交道的"素体" |
| 抽象装饰器 | java.io.FilterInputStream |
💡 很多人不知道它的存在!持有 InputStream 引用,默认全部转发 |
| 具体装饰器 | BufferedInputStream、DataInputStream |
继承 FilterInputStream,添加缓冲/数据转换等新功能 |
📝 注:
ObjectInputStream在功能上是装饰器,但由于历史原因直接继承了InputStream而非FilterInputStream。严格遵循标准路线的是BufferedInputStream和DataInputStream。
关键启示
如果明天存档文件需要加密,你只需在中间再套一层 CipherInputStream(解密装饰器)------其他层代码完全不用改。这就是装饰器模式"即插即用"的威力。
六、辨别标准:装饰器模式是否"正宗"?
判断一个设计模式是否严格符合 GoF(Gang of Four)的原意,有两个硬性指标:
6.1 结构指标:既是 Is-A,又是 Has-A
真正的装饰器必须满足一个看似矛盾的结构:
- Is-A(实现公共接口):
BufferedInputStream继承了InputStream。这保证了无论包多少层,调用方都可以把它当作最基础的流来使用------这叫透明性。 - Has-A(内部持有引用):
BufferedInputStream内部有protected volatile InputStream in。这保证了它可以把核心工作委派给底层的"素体"------这叫委派。
在 Java IO 中,JDK 甚至专门为此设计了 FilterInputStream 这个抽象装饰器基类;Servlet 规范也提供了 HttpServletResponseWrapper。连"偷懒用的父类"都帮你建好了,可见这个结构有多标准。
6.2 意图指标:运行时动态组合,而非编译时写死
如果结构看起来像装饰器,但组合关系在编译阶段就定死了,那它顶多算"静态代理"。正宗装饰器必须具备运行时即插即用的自由度:
- Java IO: 你可以根据文件类型、网络环境,在运行期间 决定套
Buffered还是Zip解压。 - SpringMVC: 框架在运行时发现这是
HEAD请求,才临时起意 把响应对象塞进HttpHeadResponseDecorator。如果是GET请求,这层装饰根本不会出现。
✅ 一句话判别: 客户端代码只依赖抽象接口,装饰器的装配在运行时由外部(工厂、框架、配置)决定 = 正宗。如果装饰关系在代码里用
new写死,那只是静态代理。
七、总结
7.1 优点
- 低耦合: 装饰类和被装饰类可以独立变化,互不影响。
- 灵活性: 可以在运行时动态添加或撤销功能。
- 替代继承: 提供了一种比继承更灵活、更可控的扩展对象功能的方式。
- 单一职责: 每个装饰器只关注一项增强,符合单一职责原则。
7.2 缺点
- 复杂性: 多层装饰会增加系统中类的数量,调试时可能需要追踪很长的调用链。
- 依赖顺序: 某些装饰场景下,装饰器的嵌套顺序会影响结果,需要仔细设计。
7.3 使用建议
- 当需要动态扩展功能时,优先考虑装饰器模式而非继承。
- 保持装饰者和具体组件的接口一致,确保透明性。
- 如果某个功能的增强是固定的、全局的,用继承可能更简单直接------不要为了设计模式而设计模式。
7.4 注意事项
- 装饰器模式可以替代继承,但应谨慎使用,避免过度装饰导致系统复杂。
- 如果装饰器之间存在顺序依赖,需要在文档中明确说明。
- 装饰器模式 vs 代理模式:装饰器侧重增强功能 ,代理侧重控制访问------虽然结构相似,但意图不同。
📚 参考: Gang of Four《设计模式:可复用面向对象软件的基础》--- Decorator Pattern