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

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);}// ========== 第三方 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,顾客可以自由加牛奶、加糖、加奶油。每种配料都会影响最终价格和描述。如果为每种组合建一个子类(CoffeeWithMilkCoffeeWithMilkAndSugar……),类数量会爆炸。

装饰器模式的解决思路:用一系列包装器对象去“包裹”核心对象,每个包装器在核心行为前后添加自己的功能。接口从未改变,始终是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接口完全不同;
  • 装饰器从未改变接口SimpleCoffeeMilkSugar都实现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);

为什么是适配器?

  • InputStreamReader两个不同的抽象类,接口完全不同;
  • InputStreamReader继承了Reader,内部持有一个InputStream实例;
  • 它在read()方法中调用InputStream.read(),并完成字节→字符的转换
  • 这就是适配器的核心——改变接口 + 转换格式

4.2BufferedInputStream(装饰器模式)

// BufferedInputStream 是典型的装饰器// Component: InputStream// ConcreteComponent: FileInputStream// Decorator: FilterInputStream(抽象装饰器)// ConcreteDecorator: BufferedInputStreamInputStreamin=newBufferedInputStream(newFileInputStream("data.bin"));

为什么是装饰器?

  • FileInputStreamBufferedInputStream都继承自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智能调度实战》。技术相通,思路可鉴。