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

摘要:适配器模式和装饰器模式都通过“包装”对象来工作,但适配器的核心是改变接口让不兼容的类协同工作,装饰器的核心是不改变接口动态增强功能。本文从物流轨迹对接和咖啡加料两个完整场景出发,结合详细代码实现、UML 对比分析和 JDK 源码(InputStreamReader vs BufferedInputStream),帮你彻底分清这对“包装型”结构型模式。

🗺️ 本文阅读地图(3 分钟速览)

  • 为什么对接第三方 SDK 要用适配器?
  • 为什么咖啡加料不用继承而用装饰器?
  • UML 对比:一个改接口,一个不改接口
  • 手写物流轨迹适配器 vs 咖啡加料装饰器,完整代码对比
  • JDK InputStreamReader(适配器)vs BufferedInputStream(装饰器)源码级分析
  • 面试必问:“适配器和装饰器怎么区分?InputStreamReader 用了哪个?”

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 当前:番外 · 适配器 vs 装饰器
🔗 返回系列总目录



1. 从“物流轨迹对接”和“咖啡加料”两个场景说起

1.1 场景一:物流轨迹对接(适配器模式)

第三方物流 SDK 提供了 LogisticsTracker 类,方法签名是 queryTrajectory(String waybillNo, String carrierCode),返回 JSON 字符串。但你整个 WMS 系统统一使用的是 TrajectoryService 接口——参数是 TrajectoryQuery 对象,返回的是 TrajectoryResult 对象。接口格式完全对不上

更麻烦的是,第三方 SDK 的源码不能修改,而你的业务代码已经深度依赖了 TrajectoryService 接口。

适配器模式的解决思路:创建一个“转接头”,实现系统统一的 TrajectoryService 接口,内部持有第三方 LogisticsTracker,完成参数转换、调用转发和结果转换。

// ========== 系统已有的目标接口(Target) ==========
public interface TrajectoryService {
    TrajectoryResult query(TrajectoryQuery query);
}

// ========== 第三方 SDK(Adaptee,不可修改) ==========
public class LogisticsTracker {
    public String queryTrajectory(String waybillNo, String carrierCode) {
        // 实际会调用第三方 API
        return "{\"status\":\"运输中\",\"location\":\"北京分拨中心\"}";
    }
}

// ========== 对象适配器(推荐) ==========
public class LogisticsTrackerAdapter implements TrajectoryService {
    private LogisticsTracker tracker;  // 持有 Adaptee

    public LogisticsTrackerAdapter(LogisticsTracker tracker) {
        this.tracker = tracker;
    }

    @Override
    public TrajectoryResult query(TrajectoryQuery query) {
        // 1. 参数转换:把 WMS 统一参数转为第三方需要的格式
        String waybillNo = query.getWaybillNo();
        String carrierCode = query.getCarrier();
        // 2. 调用第三方方法
        String jsonResponse = tracker.queryTrajectory(waybillNo, carrierCode);
        // 3. 结果转换:把 JSON 转为 WMS 统一的结果对象
        return parseJsonToResult(jsonResponse);
    }

    private TrajectoryResult parseJsonToResult(String json) {
        TrajectoryResult result = new TrajectoryResult();
        if (json.contains("运输中")) result.setStatus("运输中");
        if (json.contains("北京分拨中心")) result.setLocation("北京分拨中心");
        return result;
    }
}

// ========== 客户端调用 ==========
LogisticsTracker thirdPartyTracker = new LogisticsTracker();
TrajectoryService adapter = new LogisticsTrackerAdapter(thirdPartyTracker);
TrajectoryResult result = adapter.query(query);  // 业务代码只依赖 TrajectoryService

💬 白话:适配器就像一个“转接头”,把第三方 SDK 的接口转成我们系统统一的接口。业务代码完全不知道底层调用的是哪个 SDK。

1.2 场景二:咖啡加料(装饰器模式)

基础饮品是 SimpleCoffee,顾客可以自由加牛奶、加糖、加奶油。每种配料都会影响最终价格和描述。如果为每种组合建一个子类(CoffeeWithMilkCoffeeWithMilkAndSugar……),类数量会爆炸。

装饰器模式的解决思路:用一系列包装器对象去“包裹”核心对象,每个包装器在核心行为前后添加自己的功能。接口从未改变,始终是 Beverage

// ========== 抽象构件 ==========
public interface Beverage {
    String getDescription();
    double cost();
}

// ========== 具体构件:基础咖啡 ==========
public class SimpleCoffee implements Beverage {
    @Override
    public String getDescription() { return "黑咖啡"; }
    @Override
    public double cost() { return 10.0; }
}

// ========== 抽象装饰器 ==========
public abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;   // 被装饰的对象

    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }
}

// ========== 具体装饰器 ==========
public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) { super(beverage); }
    @Override
    public String getDescription() { return beverage.getDescription() + " + 牛奶"; }
    @Override
    public double cost() { return beverage.cost() + 3.0; }
}

public class Sugar extends CondimentDecorator {
    public Sugar(Beverage beverage) { super(beverage); }
    @Override
    public String getDescription() { return beverage.getDescription() + " + 糖"; }
    @Override
    public double cost() { return beverage.cost() + 1.0; }
}

// ========== 客户端调用 ==========
Beverage coffee = new SimpleCoffee();
coffee = new Milk(coffee);      // 加牛奶
coffee = new Sugar(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.1 InputStreamReader(适配器模式)

// InputStreamReader 是典型的对象适配器
// Target: Reader(字符流)
// Adaptee: InputStream(字节流)
// Adapter: InputStreamReader 持有 InputStream,将其适配为 Reader

Reader reader = new InputStreamReader(
    new FileInputStream("data.txt"), StandardCharsets.UTF_8
);

为什么是适配器?

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

4.2 BufferedInputStream(装饰器模式)

// BufferedInputStream 是典型的装饰器
// Component: InputStream
// ConcreteComponent: FileInputStream
// Decorator: FilterInputStream(抽象装饰器)
// ConcreteDecorator: BufferedInputStream

InputStream in = new BufferedInputStream(new FileInputStream("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 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐