当前位置: 首页 > news >正文

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+ 风格)

publicinterfaceBeverage{StringgetDescription();doublecost();}

4.2 具体构件:基础咖啡

publicclassSimpleCoffeeimplementsBeverage{@OverridepublicStringgetDescription(){return"黑咖啡";}@Overridepublicdoublecost(){return10.0;}}

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

4.3 抽象装饰器

publicabstractclassCondimentDecoratorimplementsBeverage{protectedBeveragebeverage;// 被装饰的对象(组合优于继承)publicCondimentDecorator(Beveragebeverage){this.beverage=beverage;}// 将请求转发给被装饰对象,子类可重写扩展@OverridepublicStringgetDescription(){returnbeverage.getDescription();}@Overridepublicdoublecost(){returnbeverage.cost();}}

4.4 具体装饰器

publicclassMilkextendsCondimentDecorator{publicMilk(Beveragebeverage){super(beverage);}@OverridepublicStringgetDescription(){returnbeverage.getDescription()+" + 牛奶";}@Overridepublicdoublecost(){returnbeverage.cost()+3.0;}}publicclassSugarextendsCondimentDecorator{publicSugar(Beveragebeverage){super(beverage);}@OverridepublicStringgetDescription(){returnbeverage.getDescription()+" + 糖";}@Overridepublicdoublecost(){returnbeverage.cost()+1.0;}}publicclassWhipextendsCondimentDecorator{publicWhip(Beveragebeverage){super(beverage);}@OverridepublicStringgetDescription(){returnbeverage.getDescription()+" + 奶油";}@Overridepublicdoublecost(){returnbeverage.cost()+4.0;}}

4.5 客户端调用

varcoffee=newSimpleCoffee();System.out.println(coffee.getDescription()+" 价格:"+coffee.cost());// 黑咖啡 价格:10.0coffee=newMilk(coffee);System.out.println(coffee.getDescription()+" 价格:"+coffee.cost());// 黑咖啡 + 牛奶 价格:13.0coffee=newSugar(coffee);coffee=newWhip(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+ 中,可以使用函数式接口来实现更轻量的装饰器:

importjava.util.function.Function;// 定义增强器函数(T -> T,输入和输出同类型)Function<Beverage,Beverage>withMilk=base->newBeverage(){@OverridepublicStringgetDescription(){returnbase.getDescription()+" + 牛奶";}@Overridepublicdoublecost(){returnbase.cost()+3.0;}};Function<Beverage,Beverage>withSugar=base->newBeverage(){@OverridepublicStringgetDescription(){returnbase.getDescription()+" + 糖";}@Overridepublicdoublecost(){returnbase.cost()+1.0;}};// 函数式组合varenhanced=withMilk.andThen(withSugar).apply(newSimpleCoffee());System.out.println(enhanced.getDescription()+" 价格:"+enhanced.cost());// 黑咖啡 + 牛奶 + 糖 价格:14.0

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


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

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

publicinterfaceDataReader{StringreadData();}publicclassFileDataReaderimplementsDataReader{@OverridepublicStringreadData(){return"原始数据";}}publicclassEncryptedDataReaderextendsDataReaderDecorator{publicEncryptedDataReader(DataReaderreader){super(reader);}@OverridepublicStringreadData(){return"解密("+super.readData()+")";}}publicclassCompressedDataReaderextendsDataReaderDecorator{publicCompressedDataReader(DataReaderreader){super(reader);}@OverridepublicStringreadData(){return"解压缩("+super.readData()+")";}}

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

// 代码书写顺序:从外到内varreader=newEncryptedDataReader(newCompressedDataReader(newFileDataReader()));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
varin=newBufferedInputStream(newDataInputStream(newFileInputStream("data.bin")));

9.2Collections.synchronizedList()

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

List<String>list=newArrayList<>();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智能调度实战》。技术相通,思路可鉴。

http://www.jsqmd.com/news/1019040/

相关文章:

  • 20斤以上的快递寄哪家便宜?20斤大件快递寄哪家最省钱?实测对比告诉你答案 - 快递物流资讯
  • 工业HMI设计实战:基于PXD10微控制器的集成方案与优化
  • 如何在Mac上无缝运行Windows应用?Whisky为你打开新世界的大门
  • Locale Remulator终极指南:如何彻底解决64位应用程序的转区乱码问题
  • Corazonin (Periplaneta americana)
  • 二手电瓶车托运避坑指南 交易寄运常见坑与安全保障方法?二手电瓶车托运怎么避坑?这几点不注意亏大了 - 快递物流资讯
  • 避坑指南:SAP VF04开票增强,合并开票时循环逻辑千万别这么写!
  • 别再死记硬背了!用这10个Qt面试题背后的真实项目场景,帮你真正理解原理
  • 排查DataWorks ODPS任务失败的5个高频‘非代码’原因(附真实案例)
  • i.MX VPU硬件加速接口深度解析:从统一API到实战优化
  • 如何可视化DeepLab_v3训练过程:TensorBoard监控与调试技巧
  • 2026年6月海安车灯升级到店检查怎么问?车型、问题和用车场景到店前先说清 - Ayu8888
  • 戴尔笔记本风扇控制的终极指南:如何让您的设备安静而高效
  • 广州中药提取设备四家主流厂商盘点 2026年选型参考指南 - 信息热点
  • Java计算机毕设之基于 SpringBoot 的三七药材产销一体化服务平台研发 中药材原产地直售视角下三七销售系统(完整前后端代码+说明文档+LW,调试定制等)
  • 浏览器扩展智能诊断:7步构建自动化故障排除系统
  • LLM客户端策略层蒸发:从协议栈瘦身到零信任路由
  • lazywarden性能优化:如何提升备份速度和降低资源消耗
  • 如何用Akagi麻将AI助手在30天内从新手变高手:10个实战技巧
  • 媞娜团队:新疆小团服务基准与伴侣出行对照 - 老张爱旅游
  • 如何快速搭建智能数字人对话系统:面向初学者的完整指南
  • 实战拆解|朴素RAG、进阶RAG、多轮RAG核心区别与落地场景
  • 最大的成长陷阱,不是停止学习,而是停止发布
  • 2026年6月成都宝总推荐,成都宝总餐饮/成都宝总/成都宝总餐饮电商,成都宝总培训课程好吗 - 品牌推荐师
  • 如何一键解决Windows运行库问题:VisualCppRedist AIO完全指南
  • FanControl终极指南:三步解决Windows电脑散热难题
  • HunterPie实战指南:5步掌握《怪物猎人世界》智能覆盖层监控
  • 商丘装修深度选购指南本地装企避坑+行业盘点,改善型家装怎么选不踩雷 - 国麟测评
  • 别再死记硬背了!SparkStreaming直连Kafka的5个关键配置项详解(附避坑清单)
  • 轻规划鸿蒙开发实战10:分布式数据同步深度博弈,UserId 隔离与并发数据冲突消解机