Java 23 种设计模式:从踩坑到精通 | 装饰器模式 ------ 比继承更灵活的扩展方式,你用过吗?
摘要 :当需要为对象动态添加功能时,继承会导致子类膨胀且不够灵活。装饰器模式通过"包装"的方式,在不改变原有类的情况下透明地增强对象,支持多层嵌套和运行时组合。本文从"一杯咖啡"的计价场景出发,完整讲解透明装饰与半透明装饰的实现,结合 Java I/O、Spring 缓存等框架源码,并引入函数式接口 与 Record 类等现代 Java 写法,帮你掌握"组合优于继承"的核心设计思维。
📖 《Java 23 种设计模式:从踩坑到精通》开篇:系列介绍与目录 | 上一篇:组合模式 | 当前:装饰器模式 | 下一篇:外观模式
🔗 返回系列总目录
1. 从一杯"加料"咖啡说起
你经营一家咖啡店,基础饮品是 SimpleCoffee,但顾客可以自由加牛奶、加糖、加奶油,每种配料都会影响最终价格。如果为每种组合建一个子类(如 CoffeeWithMilk、CoffeeWithMilkAndSugar......),不出多久就会得到十多个子类,且新增配料时牵一发动全身。
直接修改 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 动态代理、远程代理、延迟加载 |
| 添加功能方式 | 一层包一层,可组合 | 通常在代理内部统一处理 |
💡 简单记忆 :装饰器是"加料",代理是"中介"。
BufferedInputStream给FileInputStream加缓冲,是装饰;Spring AOP 给 Service 加事务,是代理。
9. 框架与实践中的应用
9.1 Java I/O 流体系
- Component :
InputStream(抽象构件) - ConcreteComponent :
FileInputStream(具体构件) - Decorator :
FilterInputStream(抽象装饰器) - ConcreteDecorator :
BufferedInputStream、DataInputStream、PushbackInputStream等
java
var in = new BufferedInputStream(
new DataInputStream(
new FileInputStream("data.bin")));
9.2 Collections.synchronizedList()
SynchronizedList 是 List 的装饰器,在每个方法外包装了 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。请帮我创建两个装饰器类:MilkDecorator和SugarDecorator,要求符合 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智能调度实战》。技术相通,思路可鉴。