
Java 23 种设计模式从踩坑到精通 | 装饰器模式 —— 比继承更灵活的扩展方式你用过吗摘要当需要为对象动态添加功能时继承会导致子类膨胀且不够灵活。装饰器模式通过“包装”的方式在不改变原有类的情况下透明地增强对象支持多层嵌套和运行时组合。本文从“一杯咖啡”的计价场景出发完整讲解透明装饰与半透明装饰的实现结合 Java I/O、Spring 缓存等框架源码并引入函数式接口与Record 类等现代 Java 写法帮你掌握“组合优于继承”的核心设计思维。《Java 23 种设计模式从踩坑到精通》开篇系列介绍与目录 上一篇组合模式 当前装饰器模式 下一篇外观模式 返回系列总目录1. 从一杯“加料”咖啡说起你经营一家咖啡店基础饮品是SimpleCoffee但顾客可以自由加牛奶、加糖、加奶油每种配料都会影响最终价格。如果为每种组合建一个子类如CoffeeWithMilk、CoffeeWithMilkAndSugar……不出多久就会得到十多个子类且新增配料时牵一发动全身。直接修改SimpleCoffee类也不行——单一职责和开闭原则都不同意。更麻烦的是如果需要在运行时根据用户选择动态组合配料静态的继承根本无法胜任。装饰器模式Decorator Pattern的解决思路是用一系列包装器对象去“包裹”核心对象每个包装器在核心行为前后添加自己的功能再将调用转发给被包装对象。就像俄罗斯套娃一层套一层每一层都增加一点新功能。1.1 你的场景该不该用装饰器判断标准是 → 用装饰器否 → 用其他方式需要动态、透明地给对象添加功能✅❌功能可以任意组合且组合顺序可能影响结果✅❌不想用继承导致子类膨胀✅❌功能增强是固定的且不会动态变化❌直接修改类或使用继承只是简单增强一个方法不涉及多个方法协调❌优先考虑函数式接口Lambda2. 模式定义与 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.beveragebeverage;}// 将请求转发给被装饰对象子类可重写扩展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 客户端调用varcoffeenewSimpleCoffee();System.out.println(coffee.getDescription() 价格coffee.cost());// 黑咖啡 价格10.0coffeenewMilk(coffee);System.out.println(coffee.getDescription() 价格coffee.cost());// 黑咖啡 牛奶 价格13.0coffeenewSugar(coffee);coffeenewWhip(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输入和输出同类型FunctionBeverage,BeveragewithMilkbase-newBeverage(){OverridepublicStringgetDescription(){returnbase.getDescription() 牛奶;}Overridepublicdoublecost(){returnbase.cost()3.0;}};FunctionBeverage,BeveragewithSugarbase-newBeverage(){OverridepublicStringgetDescription(){returnbase.getDescription() 糖;}Overridepublicdoublecost(){returnbase.cost()1.0;}};// 函数式组合varenhancedwithMilk.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());}}关键嵌套顺序决定执行顺序// 代码书写顺序从外到内varreadernewEncryptedDataReader(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 动态代理、远程代理、延迟加载添加功能方式一层包一层可组合通常在代理内部统一处理简单记忆装饰器是“加料”代理是“中介”。BufferedInputStream给FileInputStream加缓冲是装饰Spring AOP 给 Service 加事务是代理。9. 框架与实践中的应用9.1 Java I/O 流体系ComponentInputStream抽象构件ConcreteComponentFileInputStream具体构件DecoratorFilterInputStream抽象装饰器ConcreteDecoratorBufferedInputStream、DataInputStream、PushbackInputStream等varinnewBufferedInputStream(newDataInputStream(newFileInputStream(data.bin)));9.2Collections.synchronizedList()SynchronizedList是List的装饰器在每个方法外包装了synchronized块将非线程安全的列表装饰为线程安全。ListStringlistnewArrayList();ListStringsyncListCollections.synchronizedList(list);9.3 Spring 中的TransactionAwareCacheDecoratorSpring 通过装饰器模式增强缓存管理TransactionAwareCacheDecorator包装原始Cache对象使其支持事务感知——只有事务提交后才真正写入缓存。9.4 Spring WebFluxServerWebExchangeDecorator在 Spring Cloud Gateway 中ServerWebExchangeDecorator用于装饰网关请求/响应添加自定义的请求头修改、日志记录等功能体现了装饰器模式在响应式编程中的应用。10. AI 时代的装饰器模式推荐 Prompt“我有一个Beverage接口以及它的基础实现SimpleCoffee。请帮我创建两个装饰器类MilkDecorator和SugarDecorator要求符合 SOLID 原则支持线程安全的链式组合并提供 JUnit 5 单元测试。”还可以更进一步让 AI 帮你判断何时用装饰器、何时用函数式替代“当前业务中Beverage只需要增强cost()一个方法我不希望写太多类。请帮我用FunctionBeverage, 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智能调度实战》。技术相通思路可鉴。