走进 Gang of Four 设计模式:装饰器模式

走进 Gang of Four 设计模式:装饰器模式


一、概念:什么是装饰器模式?

装饰器模式(Decorator Pattern)是一种结构型设计模式 ,它允许你在不改变原有对象结构的前提下,动态地向对象添加新功能。

通俗地讲:你把一个对象当作"素体",然后用一个或多个"装饰器"把它一层层包裹起来。每一层包裹都可以在原功能的基础上增加新的行为,而最外层的包装器和最里层的素体对外暴露的是同一套接口------调用方根本感知不到自己被"装饰"了。

官方定义: 动态地给一个对象添加额外的职责,同时不改变其结构。装饰器模式提供了一种灵活的替代继承方式来扩展功能。


二、动机:为什么要用装饰器模式?

2.1 它解决了什么问题?

在面向对象设计中,我们通常用继承 来扩展类的功能。但继承有一个致命缺陷:它在编译期就把功能组合写死了

举个例子:你有一个"窗口"类,想给它加边框、加滚动条、加阴影。如果用继承,你需要写:

  • WindowWithBorder
  • WindowWithScrollbar
  • WindowWithShadow
  • WindowWithBorderAndScrollbar
  • WindowWithBorderAndShadow
  • WindowWithScrollbarAndShadow
  • WindowWithBorderAndScrollbarAndShadow

三个功能就爆出 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

观察: redCircleredRectangle 在原有 draw() 行为的基础上,无侵入地 增加了"红色边框"功能。原始的 CircleRectangle 类没有任何改动。


五、深入理解:两个经典实战案例

上面的 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 处理器时:

  1. 创建装饰器: 拿出原生 HttpServletResponse(真正连着网卡的那个对象),把它塞进 HttpHeadResponseDecorator 里。
  2. 欺骗 Controller: 把这个装饰器对象传给 downloadVideo 方法。你的代码完全不知道传进来的是"假货"------因为它们实现了同样的接口。
  3. 放行 Header: response.setContentType() → 装饰器立刻转发给肚子里的真实对象。
  4. 吞噬 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 类,包含血量、等级、装备)。但硬盘只认识 01,不认识 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() 等基本方法的抽象类
具体组件 FileInputStreamSocketInputStream 真正跟文件系统/网络打交道的"素体"
抽象装饰器 java.io.FilterInputStream 💡 很多人不知道它的存在!持有 InputStream 引用,默认全部转发
具体装饰器 BufferedInputStreamDataInputStream 继承 FilterInputStream,添加缓冲/数据转换等新功能

📝 注: ObjectInputStream 在功能上是装饰器,但由于历史原因直接继承了 InputStream 而非 FilterInputStream。严格遵循标准路线的是 BufferedInputStreamDataInputStream

关键启示

如果明天存档文件需要加密,你只需在中间再套一层 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

相关推荐
云恒要逆袭1 小时前
Java类型转换详解:小数字转大自动跑,大数字转小要小心
java·后端
星辰_mya2 小时前
openfeign之在回首
java·架构·dubbo·springcloud·openfeign
青山木2 小时前
Hot 100 --- 滑动窗口最大值
java·数据结构·算法·leetcode·动态规划
青山木2 小时前
Hot 100 --- 除自身以外数组的乘积
java·数据结构·算法
invicinble2 小时前
关于springsecurity技术栈,逻辑概念的总结
spring boot
Sam09272 小时前
Java 转 AI Agent 开发:Java 和 Python 的区别与快速学习指南
java·人工智能·python·ai
heimeiyingwang2 小时前
【架构实战】数据脱敏与隐私保护:合规是底线
java·开发语言·架构
dengyuezhe80602 小时前
《C++ 异常机制与智能指针:从原理到实现》
android·java·c++
于指尖飞舞2 小时前
java后端面试题(常用集合极简)
java·开发语言·面试