Java 23 种设计模式:从踩坑到精通 | 番外:适配器 vs 装饰器 —— 一个改接口,一个不改接口

发布时间:2026/7/15 22:54:26
Java 23 种设计模式:从踩坑到精通 | 番外:适配器 vs 装饰器 —— 一个改接口,一个不改接口 Java 23 种设计模式从踩坑到精通 | 番外适配器 vs 装饰器 —— 一个改接口一个不改接口摘要适配器模式和装饰器模式都通过“包装”对象来工作但适配器的核心是改变接口让不兼容的类协同工作装饰器的核心是不改变接口动态增强功能。本文从物流轨迹对接和咖啡加料两个完整场景出发结合详细代码实现、UML 对比分析和 JDK 源码InputStreamReadervsBufferedInputStream帮你彻底分清这对“包装型”结构型模式。️本文阅读地图3 分钟速览为什么对接第三方 SDK 要用适配器为什么咖啡加料不用继承而用装饰器UML 对比一个改接口一个不改接口手写物流轨迹适配器 vs 咖啡加料装饰器完整代码对比JDKInputStreamReader适配器vsBufferedInputStream装饰器源码级分析面试必问“适配器和装饰器怎么区分InputStreamReader用了哪个”《Java 23 种设计模式从踩坑到精通》开篇系列介绍与目录 当前番外 · 适配器 vs 装饰器 返回系列总目录文章目录Java 23 种设计模式从踩坑到精通 | 番外适配器 vs 装饰器 —— 一个改接口一个不改接口1. 从“物流轨迹对接”和“咖啡加料”两个场景说起1.1 场景一物流轨迹对接适配器模式1.2 场景二咖啡加料装饰器模式1.3 关键差异已浮现2. UML 对比3. 三大核心区别4. JDK 源码深度对比4.1 InputStreamReader适配器模式4.2 BufferedInputStream装饰器模式5. 什么时候不该用6. 面试必问 追问连环炮 《Java 23 种设计模式从踩坑到精通》快速导航1. 从“物流轨迹对接”和“咖啡加料”两个场景说起1.1 场景一物流轨迹对接适配器模式第三方物流 SDK 提供了LogisticsTracker类方法签名是queryTrajectory(String waybillNo, String carrierCode)返回 JSON 字符串。但你整个 WMS 系统统一使用的是TrajectoryService接口——参数是TrajectoryQuery对象返回的是TrajectoryResult对象。接口格式完全对不上。更麻烦的是第三方 SDK 的源码不能修改而你的业务代码已经深度依赖了TrajectoryService接口。适配器模式的解决思路创建一个“转接头”实现系统统一的TrajectoryService接口内部持有第三方LogisticsTracker完成参数转换、调用转发和结果转换。// 系统已有的目标接口Target publicinterfaceTrajectoryService{TrajectoryResultquery(TrajectoryQueryquery);}// 第三方 SDKAdaptee不可修改 publicclassLogisticsTracker{publicStringqueryTrajectory(StringwaybillNo,StringcarrierCode){// 实际会调用第三方 APIreturn{\status\:\运输中\,\location\:\北京分拨中心\};}}// 对象适配器推荐 publicclassLogisticsTrackerAdapterimplementsTrajectoryService{privateLogisticsTrackertracker;// 持有 AdapteepublicLogisticsTrackerAdapter(LogisticsTrackertracker){this.trackertracker;}OverridepublicTrajectoryResultquery(TrajectoryQueryquery){// 1. 参数转换把 WMS 统一参数转为第三方需要的格式StringwaybillNoquery.getWaybillNo();StringcarrierCodequery.getCarrier();// 2. 调用第三方方法StringjsonResponsetracker.queryTrajectory(waybillNo,carrierCode);// 3. 结果转换把 JSON 转为 WMS 统一的结果对象returnparseJsonToResult(jsonResponse);}privateTrajectoryResultparseJsonToResult(Stringjson){TrajectoryResultresultnewTrajectoryResult();if(json.contains(运输中))result.setStatus(运输中);if(json.contains(北京分拨中心))result.setLocation(北京分拨中心);returnresult;}}// 客户端调用 LogisticsTrackerthirdPartyTrackernewLogisticsTracker();TrajectoryServiceadapternewLogisticsTrackerAdapter(thirdPartyTracker);TrajectoryResultresultadapter.query(query);// 业务代码只依赖 TrajectoryService白话适配器就像一个“转接头”把第三方 SDK 的接口转成我们系统统一的接口。业务代码完全不知道底层调用的是哪个 SDK。1.2 场景二咖啡加料装饰器模式基础饮品是SimpleCoffee顾客可以自由加牛奶、加糖、加奶油。每种配料都会影响最终价格和描述。如果为每种组合建一个子类CoffeeWithMilk、CoffeeWithMilkAndSugar……类数量会爆炸。装饰器模式的解决思路用一系列包装器对象去“包裹”核心对象每个包装器在核心行为前后添加自己的功能。接口从未改变始终是Beverage。// 抽象构件 publicinterfaceBeverage{StringgetDescription();doublecost();}// 具体构件基础咖啡 publicclassSimpleCoffeeimplementsBeverage{OverridepublicStringgetDescription(){return黑咖啡;}Overridepublicdoublecost(){return10.0;}}// 抽象装饰器 publicabstractclassCondimentDecoratorimplementsBeverage{protectedBeveragebeverage;// 被装饰的对象publicCondimentDecorator(Beveragebeverage){this.beveragebeverage;}}// 具体装饰器 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;}}// 客户端调用 BeveragecoffeenewSimpleCoffee();coffeenewMilk(coffee);// 加牛奶coffeenewSugar(coffee);// 加糖System.out.println(coffee.getDescription() 价格coffee.cost());// 黑咖啡 牛奶 糖 价格14.0白话装饰器像俄罗斯套娃一层套一层每一层都增加一点新功能。最厉害的是——所有层都实现同一个Beverage接口客户端完全不用区分“这是基础咖啡还是加了牛奶的咖啡”。1.3 关键差异已浮现适配器改变了接口客户端用的TrajectoryService和第三方 SDK 的LogisticsTracker接口完全不同装饰器从未改变接口SimpleCoffee、Milk、Sugar都实现Beverage客户端始终面向Beverage编程。2. UML 对比3. 三大核心区别对比维度适配器模式装饰器模式是否改变接口✅ 改变Target ≠ Adaptee❌ 不改变始终是 Component意图解决接口不兼容让不兼容的类协同工作动态增强功能不修改原有代码嵌套能力通常一对一可层层嵌套任意组合客户端感知客户端面向 Target 接口不感知 Adaptee客户端始终面向 Component 接口典型应用InputStreamReader字节流→字符流BufferedInputStream给字节流加缓冲4. JDK 源码深度对比4.1InputStreamReader适配器模式// InputStreamReader 是典型的对象适配器// Target: Reader字符流// Adaptee: InputStream字节流// Adapter: InputStreamReader 持有 InputStream将其适配为 ReaderReaderreadernewInputStreamReader(newFileInputStream(data.txt),StandardCharsets.UTF_8);为什么是适配器InputStream和Reader是两个不同的抽象类接口完全不同InputStreamReader继承了Reader内部持有一个InputStream实例它在read()方法中调用InputStream.read()并完成字节→字符的转换这就是适配器的核心——改变接口 转换格式。4.2BufferedInputStream装饰器模式// BufferedInputStream 是典型的装饰器// Component: InputStream// ConcreteComponent: FileInputStream// Decorator: FilterInputStream抽象装饰器// ConcreteDecorator: BufferedInputStreamInputStreaminnewBufferedInputStream(newFileInputStream(data.bin));为什么是装饰器FileInputStream和BufferedInputStream都继承自InputStream接口从未改变BufferedInputStream只是在原有read()方法前后添加了缓冲逻辑你可以继续嵌套new DataInputStream(new BufferedInputStream(new FileInputStream()))这就是装饰器的核心——不改变接口 动态增强 可嵌套。5. 什么时候不该用滥用场景应该用什么原因两个模块都由你控制接口可以统一直接统一接口适配器是补救手段不是设计目标只想给对象加日志接口本身没变装饰器适配器会改变接口过度设计只想控制访问权限接口本身没变代理模式代理控访问装饰加功能适配改接口功能增强逻辑简单且固定直接修改类或继承装饰器引入多层嵌套调试成本高6. 面试必问 追问连环炮基础必问适配器和装饰器的核心区别→ 适配器改变接口装饰器不改变接口。InputStreamReader用了哪种模式→ 适配器模式对象适配器将InputStream适配为Reader。BufferedInputStream用了哪种模式→ 装饰器模式给InputStream添加缓冲功能接口没变。面试官追问“Collections.synchronizedList()是装饰器还是适配器” 装饰器。它包装List添加线程安全功能接口始终是List没有改变。“Collections.unmodifiableList()是装饰器还是代理” 更接近代理。它的主要目的是控制访问禁止修改而非增强功能。“对象适配器和类适配器有什么区别” 对象适配器用组合持有 Adaptee 实例更灵活可以适配多个不同的 Adaptee类适配器用继承继承 Adaptee受限于 Java 单继承。日常开发首选对象适配器。恭喜如果你能立刻说出InputStreamReader是适配器接口变了、BufferedInputStream是装饰器接口没变你已经掌握了这对“包装型”结构型模式最核心的区分点。延伸阅读本文是适配器模式与装饰器模式的深度对比。如果你想先了解每种模式的完整原理、UML、代码实现和面试题可以先阅读正篇——适配器模式让不兼容的接口也能一起工作 和 装饰器模式比继承更灵活的扩展方式再回来看这篇对比效果更佳。 《Java 23 种设计模式从踩坑到精通》快速导航开篇系列介绍与目录当前番外 · 适配器 vs 装饰器 —— 一个改接口一个不改接口你在这里 延伸阅读适配器模式正篇 装饰器模式正篇创建型模式汇总结构型模式汇总行为型模式汇总 关注《Java 23 种设计模式从踩坑到精通》用 25 篇文章彻底吃透设计模式。福利预告全系列代码及 UML 源码将在完结时统一打包开放点击「关注」「收藏」第一时间获取。 除了设计模式我也在深挖智能物流实战WMS、托盘调度、机器学习落地。欢迎点击头像看看专栏 《出版社物流WMS智能调度实战》。技术相通思路可鉴。