Java 23 种设计模式:从踩坑到精通 | 番外:适配器 vs 装饰器 —— 一个改接口,一个不改接口
Java 23 种设计模式:从踩坑到精通 | 番外:适配器 vs 装饰器 —— 一个改接口,一个不改接口
摘要:适配器模式和装饰器模式都通过“包装”对象来工作,但适配器的核心是改变接口让不兼容的类协同工作,装饰器的核心是不改变接口动态增强功能。本文从物流轨迹对接和咖啡加料两个完整场景出发,结合详细代码实现、UML 对比分析和 JDK 源码(
InputStreamReadervsBufferedInputStream),帮你彻底分清这对“包装型”结构型模式。
🗺️本文阅读地图(3 分钟速览)
- 为什么对接第三方 SDK 要用适配器?
- 为什么咖啡加料不用继承而用装饰器?
- UML 对比:一个改接口,一个不改接口
- 手写物流轨迹适配器 vs 咖啡加料装饰器,完整代码对比
- JDK
InputStreamReader(适配器)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);}// ========== 第三方 SDK(Adaptee,不可修改) ==========publicclassLogisticsTracker{publicStringqueryTrajectory(StringwaybillNo,StringcarrierCode){// 实际会调用第三方 APIreturn"{\"status\":\"运输中\",\"location\":\"北京分拨中心\"}";}}// ========== 对象适配器(推荐) ==========publicclassLogisticsTrackerAdapterimplementsTrajectoryService{privateLogisticsTrackertracker;// 持有 AdapteepublicLogisticsTrackerAdapter(LogisticsTrackertracker){this.tracker=tracker;}@OverridepublicTrajectoryResultquery(TrajectoryQueryquery){// 1. 参数转换:把 WMS 统一参数转为第三方需要的格式StringwaybillNo=query.getWaybillNo();StringcarrierCode=query.getCarrier();// 2. 调用第三方方法StringjsonResponse=tracker.queryTrajectory(waybillNo,carrierCode);// 3. 结果转换:把 JSON 转为 WMS 统一的结果对象returnparseJsonToResult(jsonResponse);}privateTrajectoryResultparseJsonToResult(Stringjson){TrajectoryResultresult=newTrajectoryResult();if(json.contains("运输中"))result.setStatus("运输中");if(json.contains("北京分拨中心"))result.setLocation("北京分拨中心");returnresult;}}// ========== 客户端调用 ==========LogisticsTrackerthirdPartyTracker=newLogisticsTracker();TrajectoryServiceadapter=newLogisticsTrackerAdapter(thirdPartyTracker);TrajectoryResultresult=adapter.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.beverage=beverage;}}// ========== 具体装饰器 ==========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;}}// ========== 客户端调用 ==========Beveragecoffee=newSimpleCoffee();coffee=newMilk(coffee);// 加牛奶coffee=newSugar(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,将其适配为 ReaderReaderreader=newInputStreamReader(newFileInputStream("data.txt"),StandardCharsets.UTF_8);为什么是适配器?
InputStream和Reader是两个不同的抽象类,接口完全不同;InputStreamReader继承了Reader,内部持有一个InputStream实例;- 它在
read()方法中调用InputStream.read(),并完成字节→字符的转换; - 这就是适配器的核心——改变接口 + 转换格式。
4.2BufferedInputStream(装饰器模式)
// BufferedInputStream 是典型的装饰器// Component: InputStream// ConcreteComponent: FileInputStream// Decorator: FilterInputStream(抽象装饰器)// ConcreteDecorator: BufferedInputStreamInputStreamin=newBufferedInputStream(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智能调度实战》。技术相通,思路可鉴。
