Java 23 种设计模式:从踩坑到精通 | 装饰器模式 —— 比继承更灵活的扩展方式,你用过吗?

Java 23 种设计模式:从踩坑到精通 | 装饰器模式 ------ 比继承更灵活的扩展方式,你用过吗?

摘要 :当需要为对象动态添加功能时,继承会导致子类膨胀且不够灵活。装饰器模式通过"包装"的方式,在不改变原有类的情况下透明地增强对象,支持多层嵌套和运行时组合。本文从"一杯咖啡"的计价场景出发,完整讲解透明装饰与半透明装饰的实现,结合 Java I/O、Spring 缓存等框架源码,并引入函数式接口Record 类等现代 Java 写法,帮你掌握"组合优于继承"的核心设计思维。
📖 《Java 23 种设计模式:从踩坑到精通》

开篇:系列介绍与目录 | 上一篇:组合模式 | 当前:装饰器模式 | 下一篇:外观模式

🔗 返回系列总目录


1. 从一杯"加料"咖啡说起

你经营一家咖啡店,基础饮品是 SimpleCoffee,但顾客可以自由加牛奶、加糖、加奶油,每种配料都会影响最终价格。如果为每种组合建一个子类(如 CoffeeWithMilkCoffeeWithMilkAndSugar......),不出多久就会得到十多个子类,且新增配料时牵一发动全身。

直接修改 SimpleCoffee 类也不行------单一职责和开闭原则都不同意。更麻烦的是,如果需要在运行时根据用户选择动态组合配料,静态的继承根本无法胜任。

装饰器模式(Decorator Pattern)的解决思路是:用一系列包装器对象去"包裹"核心对象,每个包装器在核心行为前后添加自己的功能,再将调用转发给被包装对象。就像俄罗斯套娃,一层套一层,每一层都增加一点新功能。

1.1 你的场景该不该用装饰器?

判断标准 是 → 用装饰器 否 → 用其他方式
需要动态、透明地给对象添加功能
功能可以任意组合,且组合顺序可能影响结果
不想用继承导致子类膨胀
功能增强是固定的,且不会动态变化 直接修改类或使用继承
只是简单增强一个方法,不涉及多个方法协调 优先考虑函数式接口(Lambda)

2. 模式定义与 UML 结构

装饰器模式 动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式比生成子类更灵活。它属于 结构型设计模式

四个角色

  • Component(抽象构件):定义原始对象和装饰器的公共接口;
  • ConcreteComponent(具体构件):被装饰的原始对象;
  • Decorator(抽象装饰器) :持有一个 Component 引用,将请求转发给该引用------这正是"组合优于继承"的具体体现;
  • ConcreteDecorator(具体装饰器):在转发前后添加自己的行为。

3. 透明装饰与半透明装饰

在实际使用中,根据客户端是否需要知道具体装饰器类型,分为两种风格:

  • 透明装饰 :客户端完全面向 Component 接口编程,不关心具体装饰器类型。装饰器的方法签名与 Component 完全一致,所有增强都在 operation() 内部完成。这是最理想的形式。
  • 半透明装饰 :装饰器可以提供额外的专属方法(如 addMilk()),客户端需要知道具体装饰器类型才能调用。这种写法牺牲了部分透明性,但更灵活,能更细粒度地控制增强。

4. 代码实现:咖啡计价系统

4.1 抽象构件(Java 17+ 风格)

java 复制代码
public interface Beverage {
    String getDescription();
    double cost();
}

4.2 具体构件:基础咖啡

java 复制代码
public class SimpleCoffee implements Beverage {
    @Override
    public String getDescription() {
        return "黑咖啡";
    }

    @Override
    public double cost() {
        return 10.0;
    }
}

💡 现代 Java 小贴士 :在 Java 14+ 中,如果 SimpleCoffee 只是纯数据载体,可以使用 Record 类进一步简化,但为了后续扩展性(如加日志),这里保留普通类形式。

4.3 抽象装饰器

java 复制代码
public abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;   // 被装饰的对象(组合优于继承)

    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }

    // 将请求转发给被装饰对象,子类可重写扩展
    @Override
    public String getDescription() {
        return beverage.getDescription();
    }

    @Override
    public double cost() {
        return beverage.cost();
    }
}

4.4 具体装饰器

java 复制代码
public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " + 牛奶";
    }

    @Override
    public double cost() {
        return beverage.cost() + 3.0;
    }
}

public class Sugar extends CondimentDecorator {
    public Sugar(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " + 糖";
    }

    @Override
    public double cost() {
        return beverage.cost() + 1.0;
    }
}

public class Whip extends CondimentDecorator {
    public Whip(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " + 奶油";
    }

    @Override
    public double cost() {
        return beverage.cost() + 4.0;
    }
}

4.5 客户端调用

java 复制代码
var coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " 价格:" + coffee.cost());
// 黑咖啡 价格:10.0

coffee = new Milk(coffee);
System.out.println(coffee.getDescription() + " 价格:" + coffee.cost());
// 黑咖啡 + 牛奶 价格:13.0

coffee = new Sugar(coffee);
coffee = new Whip(coffee);
System.out.println(coffee.getDescription() + " 价格:" + coffee.cost());
// 黑咖啡 + 牛奶 + 糖 + 奶油 价格:18.0

🔧 执行顺序图解 :代码书写顺序是 Whip(Sugar(Milk(SimpleCoffee))),但实际调用时,cost() 从外到内调用------先执行 Whip.cost(),它调用 Sugar.cost(),再调用 Milk.cost(),最后调用 SimpleCoffee.cost(),然后逐层返回累加价格。画在纸上是一个"俄罗斯套娃"的结构,最外层的装饰器最先拦截请求,最后返回结果。 建议在 cost() 方法处打断点,单步追踪调用栈,你会对装饰器的"洋葱模型"有更深的理解。

客户端始终面向 Beverage 接口,可任意组合装饰器,无限扩展,无需修改原有代码。


5. 现代 Java 替代方案:函数式装饰器

如果只涉及单一方法的增强(如 cost()),写 4 个类(接口 + 具体类 + 抽象装饰 + 具体装饰)确实有些"重"。在 Java 8+ 中,可以使用 函数式接口 来实现更轻量的装饰器:

java 复制代码
import java.util.function.Function;

// 定义增强器函数(T -> T,输入和输出同类型)
Function<Beverage, Beverage> withMilk = base -> new Beverage() {
    @Override
    public String getDescription() {
        return base.getDescription() + " + 牛奶";
    }

    @Override
    public double cost() {
        return base.cost() + 3.0;
    }
};

Function<Beverage, Beverage> withSugar = base -> new Beverage() {
    @Override
    public String getDescription() {
        return base.getDescription() + " + 糖";
    }

    @Override
    public double cost() {
        return base.cost() + 1.0;
    }
};

// 函数式组合
var enhanced = withMilk
    .andThen(withSugar)
    .apply(new SimpleCoffee());

System.out.println(enhanced.getDescription() + " 价格:" + enhanced.cost());
// 黑咖啡 + 牛奶 + 糖 价格:14.0

函数式装饰器的优势 :无需定义装饰器子类,代码量骤减。但代价是丢失了类型信息 ------匿名类无法用 instanceof 判断具体类型。如果业务中需要"剥开"某一层装饰器(如"去掉牛奶"),还是传统的类继承体系更合适。如果只是为了组合增强,函数式方案更轻量。


6. 代码实现:数据流加密

假设有一个基础的文件读取器,我们想在读取数据时自动进行加密、压缩等处理。

java 复制代码
public interface DataReader {
    String readData();
}

public class FileDataReader implements DataReader {
    @Override
    public String readData() {
        return "原始数据";
    }
}

public class EncryptedDataReader extends DataReaderDecorator {
    public EncryptedDataReader(DataReader reader) { super(reader); }

    @Override
    public String readData() {
        return "解密(" + super.readData() + ")";
    }
}

public class CompressedDataReader extends DataReaderDecorator {
    public CompressedDataReader(DataReader reader) { super(reader); }

    @Override
    public String readData() {
        return "解压缩(" + super.readData() + ")";
    }
}

关键:嵌套顺序决定执行顺序

java 复制代码
// 代码书写顺序:从外到内
var reader = new EncryptedDataReader(new CompressedDataReader(new FileDataReader()));
System.out.println(reader.readData());  
// 输出:解密(解压缩(原始数据))
// 执行顺序:FileDataReader → CompressedDataReader → EncryptedDataReader

⚠️ 执行顺序反直觉 :代码书写是 Encrypted(Compressed(File)),执行时却是 File → Compressed → Encrypted最内层先执行,最外层最后执行。如果顺序搞反------先加密再压缩------结果可能完全不同。在涉及加密、压缩、编码等顺序敏感的场景中,务必画出嵌套结构确认顺序。


7. 优缺点一览

优点 缺点
灵活扩展:动态、透明地增加功能,比静态继承更灵活 产生大量小类:每个功能一个装饰器,增加代码复杂度
遵循开闭原则:新增装饰器类即可,无需修改原有类 多层装饰调试困难:嵌套太深时调用栈较长,排查问题费时
组合灵活:装饰器可以任意组合,实现不同功能组合 依赖顺序:某些装饰组合对顺序敏感,容易出错
保持核心对象简单:单一职责,核心对象不膨胀 过度设计风险:简单场景下用函数式替代更合适

8. 装饰器模式 vs 代理模式

这是面试中极容易混淆的一对,核心区别在于意图:

对比维度 装饰器模式 代理模式
目的 增强或增加功能 控制访问,增加间接层
关注点 动态添加职责 控制对象访问
客户端感知 可以不知道具体装饰器(透明装饰) 通常不需要知道真实对象
典型应用 Java I/O 流、Collections.synchronizedList() AOP 动态代理、远程代理、延迟加载
添加功能方式 一层包一层,可组合 通常在代理内部统一处理

💡 简单记忆 :装饰器是"加料",代理是"中介"。BufferedInputStreamFileInputStream 加缓冲,是装饰;Spring AOP 给 Service 加事务,是代理。


9. 框架与实践中的应用

9.1 Java I/O 流体系

  • ComponentInputStream(抽象构件)
  • ConcreteComponentFileInputStream(具体构件)
  • DecoratorFilterInputStream(抽象装饰器)
  • ConcreteDecoratorBufferedInputStreamDataInputStreamPushbackInputStream
java 复制代码
var in = new BufferedInputStream(
                    new DataInputStream(
                        new FileInputStream("data.bin")));

9.2 Collections.synchronizedList()

SynchronizedListList 的装饰器,在每个方法外包装了 synchronized 块,将非线程安全的列表装饰为线程安全。

java 复制代码
List<String> list = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(list);

9.3 Spring 中的 TransactionAwareCacheDecorator

Spring 通过装饰器模式增强缓存管理,TransactionAwareCacheDecorator 包装原始 Cache 对象,使其支持事务感知------只有事务提交后才真正写入缓存。

9.4 Spring WebFlux:ServerWebExchangeDecorator

在 Spring Cloud Gateway 中,ServerWebExchangeDecorator 用于装饰网关请求/响应,添加自定义的请求头修改、日志记录等功能,体现了装饰器模式在响应式编程中的应用。


10. AI 时代的装饰器模式

推荐 Prompt

"我有一个 Beverage 接口,以及它的基础实现 SimpleCoffee。请帮我创建两个装饰器类:MilkDecoratorSugarDecorator,要求符合 SOLID 原则,支持线程安全的链式组合,并提供 JUnit 5 单元测试。"

还可以更进一步,让 AI 帮你判断何时用装饰器、何时用函数式替代:

"当前业务中,Beverage 只需要增强 cost() 一个方法,我不希望写太多类。请帮我用 Function<Beverage, Beverage> 函数式接口实现同样的功能,并给出两种方案的优劣对比。"


11. 常见误区与面试高频题

❌ 误区1:装饰器模式就是代理模式

核心区别在于意图:装饰器强调增强功能,代理强调控制访问。

❌ 误区2:装饰器一定比继承好

装饰器适合需要动态组合多种增强的场景;如果增强逻辑固定且不会变化,继承可能更简洁直观。

❌ 误区3:所有包装都是装饰器

适配器也包装对象,但它改变接口;装饰器不改变接口。

❌ 误区4:函数式方案可以完全替代装饰器模式

函数式方案轻量但不支持 instanceof 类型判断。如果业务需要"剥离"某一层装饰器,传统类继承体系更合适。

💡 面试高频追问

  • Java I/O 用了什么模式? → 装饰器模式。
  • Collections.unmodifiableList() 是装饰器还是代理? → 更接近代理,因为主要目的是控制访问(禁止修改)。
  • 装饰器模式和策略模式的区别? → 策略替换整体算法,装饰在已有行为前后添加增强。
  • 装饰器的嵌套顺序如何影响结果? → 从内到外执行,顺序敏感的场景(如加密+压缩)必须谨慎设计。

12. 六大设计原则在装饰器模式中的体现

设计原则 在装饰器模式中的体现
单一职责原则(SRP) 每种装饰器只负责一种增强,核心构件保持纯粹
开闭原则(OCP) 新增功能只需添加新装饰器类,无需修改原有代码
里氏替换原则(LSP) 装饰器与构件实现同一接口,可相互替换
依赖倒置原则(DIP) 客户端依赖抽象 Component 接口,不依赖具体装饰器
接口隔离原则(ISP) 抽象构件接口精简,装饰器只关注自己的增强逻辑
迪米特法则(LoD) 客户端只知道 Component 接口,不知内部包装层次

13. 总结

装饰器模式是"组合优于继承"的经典范例,它通过层层包装的方式,将增强逻辑从核心类中剥离,实现动态扩展。Java I/O 流、Collections 工具类、Spring 缓存都深度运用了这一模式。

最终建议:当需要动态、透明地给对象添加功能,且功能可以自由组合时,首选装饰器模式。如果只涉及单一方法的简单增强,优先考虑函数式接口 + Lambda,避免过度设计。日常开发中优先使用透明装饰,保持客户端简洁。在涉及顺序敏感的场景(如加密+压缩),务必画图确认嵌套结构。


🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。

📦 福利预告 :全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

🚀 下一篇:外观模式 ------ 给复杂系统装一个"一键启动"!🚧 即将发布,敬请关注!
📌 除了设计模式,我也在深挖智能物流实战 (WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

复制代码
相关推荐
NE_STOP1 天前
Vide Coding--AI编程工具的选择
java
码云数智-园园1 天前
C++20 Modules 模块详解
java·开发语言·spring
程序员黑豆1 天前
JDK 下载安装与配置详细教程
java·前端·ai编程
小宇宙Zz1 天前
Maven依赖冲突
java·服务器·maven
swordbob1 天前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
咖啡八杯1 天前
GoF设计模式——享元模式
java·spring·设计模式·享元模式
十五喵源码网1 天前
基于springboot2+vue2的租房管理系统
java·毕业设计·springboot·论文笔记
摇滚侠1 天前
IDEA 创建 Java 项目 手动整合 SSM 框架
java·ide·intellij-idea
源分享1 天前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm
Flittly1 天前
【AgentScope Java新手村系列】(10)实战-多Agent天气助手
java·spring boot·spring