编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 面向对象设计

  • 常见设计原则

  • 巧学设计模式

    • README
    • 单例模式设计思想
    • 工厂模式设计思想
    • 建造者模式设计思想
    • 原型模式设计思想
    • 静态代理设计模式
    • 动态代理设计模式
    • 适配器模式设计思想
      • 01.案例引入与思考
        • 1.1 痛点场景
        • 1.2 它哪里不舒服
        • 1.3 引出本篇主角
      • 02.直觉方案探索
        • 2.1 尝试:全局搜索替换——把 47 处 Payment 改成 NewBankSDK
        • 2.2 尝试:抽一个 Gateway 类,内部 if-else 路由
        • 2.3 两次失败之后——需求清单收敛
      • 03.适配器模式基础
        • 3.1 从失败中提炼的标准骨架
        • 3.2 一次调用的完整时序
        • 3.3 适配器模式定义
        • 3.4 典型使用场景
        • 3.5 三角色与结构图
      • 04.两种实现对比
        • 4.1 类适配器——继承 Adaptee
        • 4.2 对象适配器——组合 Adaptee
        • 4.3 两种实现速查
        • 4.4 默认适配器(缺省适配器 Pattern)
      • 05.用前用后效果对比
        • 5.1 🧪 回到事故现场:NewBankPaymentAdapter 改造
        • 5.2 🧪 统一多供应商:敏感词过滤改造
        • 5.3 生产场景速查
        • 5.4 经典案例:Enumeration→Iterator 的 JDK 适配
        • 5.5 封装有缺陷的第三方接口
        • 5.6 核心收益
      • 06.反面踩坑实录
        • 6.1 🚨 静默吞掉语义——错误码全映射成"失败"
        • 6.2 🚨 类适配器 + 多继承陷阱
        • 6.3 🚨 双向适配只接单向——回调丢失
        • 6.4 🚨 Adapter 里 new 临时对象→GC 抖动
        • 6.5 踩坑速查 + 替代方案
      • 07.决策树与选型
        • 7.1 该不该用适配器
        • 7.2 选型清单速查
      • 08.总结与延伸
        • 8.1 演化逻辑沉淀
        • 8.2 四模式结构辨析——代理/装饰器/桥接/适配器
        • 8.3 真实开源代码中的适配器
        • 8.4 ⚠️ 什么时候不该用适配器
        • 8.5 思考题
    • 装饰者模式设计思想
    • 外观模式设计思想
    • 桥接模式设计思想
    • 组合模式设计思想
    • 享元模式设计思想
    • 观察者模式设计思想
    • 策略者模式设计思想
    • 模版模式设计思想
    • 迭代器模式设计思想
    • 职责链模式设计思想
    • 命令模式设计思想
    • 状态模式设计思想
    • 备忘录模式设计思想
    • 中介者模式设计思想
    • 访问者模式设计思想
    • 解释器模式设计思想
    • 23种设计模式概括
    • 技术写作模板
  • 系统架构设计

  • 编程
  • 巧学设计模式
杨充
2019-07-12
目录

适配器模式设计思想

# 第三卷第7章:适配器模式设计思想

📚 本篇渐进学习节奏(建议按顺序食用)

  1. 第 01 节 · 案例引入 — 双十一前三天被迫更换支付 SDK,47 处调用点翻车
  2. 第 02 节 · 直觉探索 — 全局搜索替换 / if-else 网关,两条死路走到底
  3. 第 03 节 · 模式基础 — 从失败中提炼标准骨架——实现 Target + 组合 Adaptee
  4. 第 04 节 · 两种实现 — 类适配器 vs 对象适配器一表速查
  5. 第 05 节 · 效果对比 — 事故改造前后数据说话
  6. 第 06 节 · 反面踩坑 — 4 类适配器经典翻车 + 替代方案
  7. 第 07 节 · 决策选型 — 该不该用 Adapter?类 vs 对象?一张决策图搞定
  8. 第 08 节 · 总结延伸 — 沉淀 + 联动 + 开源 + 思考题

阅读到任一节卡壳,直接跳回上一节复盘场景。

# 目录介绍

  • 01.案例引入与思考
    • 1.1 痛点场景
    • 1.2 它哪里不舒服
    • 1.3 引出本篇主角
  • 02.直觉方案探索
    • 2.1 全局搜索替换
    • 2.2 抽Gatewayif-else路由
    • 2.3 需求清单收敛
  • 03.适配器模式基础
    • 3.1 标准骨架
    • 3.2 适配器模式定义
    • 3.3 典型使用场景
  • 04.两种实现对比
    • 4.1 类适配器继承
    • 4.2 对象适配器组合
    • 4.3 两种实现速查
  • 05.用前用后效果对比
    • 5.1 支付事故改造
    • 5.2 核心收益
  • 06.反面踩坑实录
    • 6.1 静默吞掉语义
    • 6.2 类适配器多继承陷阱
    • 6.3 双向适配丢失回调
    • 6.4 Adapter里new临时对象
    • 6.5 踩坑速查替代方案
  • 07.决策树与选型
    • 7.1 该不该用适配器
    • 7.2 选型清单速查
  • 08.总结与延伸
    • 8.1 演化逻辑沉淀
    • 8.2 模式联动边界
    • 8.3 开源实例
    • 8.4 思考题

# 01.案例引入与思考

本篇主线:对接第三方 SDK / 老系统迁移时最高频的"接口签名不一致"

# 1.1 痛点场景

🔥 模拟事故复盘 · 双十一前 3 天 · 支付通道被迫更换

10 月 28 日下午 16:00,老板拉住架构师:"原来的 XX 支付通道今天告知双十一期间费率上调 30%,公司决定本周内全量切换到 YY 通道,后天上线。" 后端组拉开代码库一看——依赖原 Payment 接口的地方有 47 处:结算 Service、退款 Service、对账 Job、商户后台、营销退费、充值,全部分散在 12 个模块。小张拉过说"那我们全局搜索替换一下 SDK 调用名就行",结果周六上线后:

  • 18:30:商户后台退款接口 500,原因 BigDecimal × 100 转 long cents 时调用点忘了粘贴 setScale(2, ROUND_HALF_UP),一笔 99.999 元退款变成了 9999 分出了什么问题你们猜;
  • 22:15:对账 Job 跪了,原因商户后台代码中有些地方走老 SDK、有些走新 SDK,订单号字段一个叫 orderId 一个叫 bizNo,双表联不上;
  • 周一上午:CTO 拍板回滚。但不能真回滚——费率今天零点已上调,需要 同时保留老 SDK + 快速接入新 SDK + 支持灰度切换。全局搜索替换这条路走不通了。

这场"紧急替换事故"暴露了一个本质问题:业务层根本不应该直接调用三方 SDK 的原生接口。就像你不会把手机直接插进中国插座 — 中间总得有个转接头。如果一开始就把 Payment 抽象接口、三方 SDK 用 Adapter 隔一层,双十一前这三个零点都不会发生。

项目里原来对接的是旧版支付 SDK,内部约定好的接口是:

public interface Payment {
    PayResult pay(String orderId, BigDecimal amount);
}
1
2
3

上层已经有几十处都在用:

public class Checkout {
    private Payment payment;  // 依赖的接口
    public void checkout(Order o) {
        PayResult r = payment.pay(o.getId(), o.getAmount());
        // ...
    }
}
1
2
3
4
5
6
7

现在老板宣布要换一家第三方 SDK,对方提供的 API 长这样:

public class NewBankSDK {
    // 参数顺序、类型、返回值都完全不一样
    public Response doTransaction(Transaction tx) { ... }
}
public class Transaction {
    public void setBizNo(String no) { ... }
    public void setAmountInCents(long cents) { ... }
}
1
2
3
4
5
6
7
8

你怎么接?最直接的做法是把所有用 Payment 的地方都改掉,直接调 NewBankSDK:

flowchart LR
    C1[Checkout] -.要改.-> N[NewBankSDK.doTransaction]
    C2[退款服务] -.要改.-> N
    C3[批量对账] -.要改.-> N
    C4[...几十处调用...] -.要改.-> N
    style N fill:#fee
1
2
3
4
5
6

# 1.2 它哪里不舒服

  • ❌ 大面积改动:几十处业务代码要跟着改,改动面失控;
  • ❌ 参数转换重复:每个调用点都要做一次"订单号 → bizNo、元 → 分"的转换,复制粘贴;
  • ❌ 回退不了:万一新 SDK 不稳定要回滚老 SDK——你得再把所有地方改回来;
  • ❌ 业务耦合三方:上层业务本来只关心"支付这个动作",结果被三方 SDK 的数据结构、方法名绑死;
  • ❌ 多选一困难:如果同时要支持"老 SDK + 新 SDK"灰度切换,代码里到处是 if-else。

# 1.3 引出本篇主角

适配器模式(Adapter)的核心思想:在调用方期望的接口和被调用方实际提供的接口之间插入一个"转换器"。调用方继续用老接口签名,适配器内部把调用翻译成新接口的调用。

flowchart LR
    C[业务层 Checkout] -->|payment.pay order amount| A[NewBankPaymentAdapter<br/>实现 Payment 接口<br/>内部翻译参数]
    A -->|sdk.doTransaction tx| N[NewBankSDK]
    style A fill:#e6f3ff
    style N fill:#fdf6e3
1
2
3
4
5

业务层一行不用改,只新增一个 NewBankPaymentAdapter 类即可。如果要切回老 SDK,也只是换一个适配器。适配器还分 类适配器(继承)和 对象适配器(组合)两种——本篇会对比两种实现的适用场景与权衡。

flowchart TD
    Start([要适配目标类]) --> Q1{能否继承目标类?}
    Q1 -->|语言不支持多继承<br/>或目标是 final| Obj[对象适配器<br/>组合 + 委托<br/>更灵活]
    Q1 -->|能继承且只需适配一个| Cls[类适配器<br/>继承 + 实现目标接口<br/>更直接]
    style Obj fill:#e6f3ff
    style Cls fill:#f0e6ff
1
2
3
4
5
6

🎯 用前用后效果对比(衔接 1.1 事故现场)

基线:47 处业务调用点,需要从老 SDK 切到新 SDK,且要支持灰度:

维度 ❌ 全局搜索替换(事故现场) ✅ 引入适配器模式
业务代码改动量 47 处分散修改 0 处(业务层只面向 Payment 接口)
新增文件 0(但每处都要重复转换逻辑) 1 个 NewBankPaymentAdapter
参数转换重复度 47 处都要写"元→分"+"orderId→bizNo" 集中在 1 个 Adapter
灰度切换实现 47 处都要插 if (灰度) { 新 } else { 老 } 一个 RoutingPaymentAdapter 内部决策
一笔金额转换写错 必然发生(手工 47 次复制粘贴) 不可能(只有一处)
回滚老 SDK 全局再搜索替换一次 注入老 Adapter 即可
单元测试 无法 Mock SDK,只能集成测试 Mock Payment 接口,纯单测
同时支持新老双通道 代码各种 if-else 分叉 两个 Adapter 共存即可

结论:适配器模式的本质是 "在你的接口和别人的接口之间,预留一层薄薄的隔离"。这一层薄薄的代码就是你的项目对外部世界的"防御工事" — 三方 SDK 改了、要换厂商了、要做灰度了,所有冲击都被这一层挡住,业务代码毫发无损。这不是过度设计,这是工程的最低保障。


# 02.直觉方案探索

为什么要学这一节:直接给你 Adapter 标准答案是很容易的——但适配器不是凭空发明的。它是在"全局搜索替换"和"if-else 网关"两条死路上撞了无数次之后才收敛出来的。

flowchart LR
    Start([Payment 接口换了]) --> W1[全局搜索替换<br/>47处各写转换] -->|致命缺陷| F1[❌ 一处写错全崩]
    Start --> W2[Gateway 内 if-else<br/>路由+翻译混在一起] -->|复杂度翻倍| F2[❌ SRP违背<br/>加新SDK改网关]
    W1 & W2 -.引出.-> Solution[✅ 对象适配器<br/>只做翻译/不关心路由]
    style F1 fill:#fee
    style F2 fill:#fee
    style Solution fill:#dfd
1
2
3
4
5
6
7

# 2.1 尝试:全局搜索替换——把 47 处 Payment 改成 NewBankSDK

【新人方案①:每个调用点直接改调新 SDK】

回到 01 节那场事故后,第一反应是"所有调老 SDK 的地方,全局替换成新 SDK":

// 方案 A:47 处调用点,每处自己写一次"元→分 + orderId→bizNo"
public class Checkout {
    public void checkout(Order o) {
        Transaction tx = new Transaction();
        tx.setBizNo(o.getId());
        tx.setAmountInCents(o.getAmount().multiply(new BigDecimal(100)).longValue());  // 元→分
        new NewBankSDK().doTransaction(tx);                                            // 新 SDK 调用
    }
}
// 退款Service、对账Job、营销退费... 47 处全要写一遍这种转换!
1
2
3
4
5
6
7
8
9
10

🧪 跑一下,看会出什么问题

// 问题 1:47 处"元→分"——某处写了 setScale 某处没写 → 99.999 元变 9999 分
// 问题 2:对账 Job 里新旧 SDK 混用——orderId vs bizNo 字段对不上 → 双表联查跪
// 问题 3:要回滚老 SDK → 再全局搜索替换 47 处 → 再来一遍事故
1
2
3

❌ 失败原因:参数转换逻辑散落 47 处——复制粘贴 = 人肉同步 = 必然翻车。一处改错,整条链路崩。

💡 反思:我们需要把"新老 SDK 的翻译口诀"写在单独一处,所有调用方自动复用。

# 2.2 尝试:抽一个 Gateway 类,内部 if-else 路由

【新人方案②:写 PaymentGateway,根据灰度开关选新/老 SDK】

既然全局替换不行,那就写一个网关,内部 if-else 决定调谁:

// 方案 B:PaymentGateway 内部 if-else 路由
public class PaymentGateway implements Payment {
    public PayResult pay(String orderId, BigDecimal amount) {
        if (grayFlag) {
            Transaction tx = new Transaction();
            tx.setBizNo(orderId);
            tx.setAmountInCents(amount.multiply(new BigDecimal(100)).longValue());
            return translate(new NewBankSDK().doTransaction(tx));
        } else {
            return oldSdk.pay(orderId, amount);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

🧪 跑一下,会发现隐藏问题——灰度切换逻辑与转换逻辑耦合

// 问题 1:GrayGateway 同时做了两件事——"选谁" + "翻译参数"——SRP 违背
// 问题 2:将来要加第三家 SDK → GrayGateway 变成三路 if-else → 上帝网关
// 问题 3:要单独 Mock 老 SDK or 新 SDK 来测转换逻辑?GrayGateway 把两者绑死了
1
2
3

❌ 失败原因:网关类把"路由决策"和"协议转换"绑在同一个类里。这其实是两个正交的职责——加一个新 SDK 不应该动路由逻辑,改路由逻辑不应该动转换逻辑。

💡 反思:我们需要一个纯粹的转换器——只负责把老接口翻译成新接口,"选谁"交给外部(DI/IoC 容器),而不是在 Adapter 里 if-else。

# 2.3 两次失败之后——需求清单收敛

必须满足 来自哪一次失败
① 参数转换逻辑只写一处,所有调用方复用 2.1 全局搜索替换失败
② 转换逻辑与路由逻辑解耦——Adapter 不关心"选谁" 2.2 网关类耦合失败
③ 换 SDK 只加 Adapter 文件,业务代码一行不动 1.2 真实事故
④ 新老 SDK 可以共存——灰度切换靠 DI 容器,不靠 if-else 1.2 真实事故

# 03.适配器模式基础

# 3.1 从失败中提炼的标准骨架

上面四条约束翻译成代码,就是适配器的灵魂——实现调用方期望的接口、内部持有被适配对象、方法内做翻译:

// ① Target:调用方期望的接口——业务层只认识这个
public interface Payment {
    PayResult pay(String orderId, BigDecimal amount);
}

// ② Adaptee:第三方 SDK 的原生接口——不能改、也不想让业务层直接依赖
public class NewBankSDK {
    public Response doTransaction(Transaction tx) { ... }
}

// ③ Adapter:翻译器——实现 Target,内部组合 Adaptee
public class NewBankPaymentAdapter implements Payment {
    private final NewBankSDK sdk = new NewBankSDK();      // 组合 Adaptee

    public PayResult pay(String orderId, BigDecimal amount) {
        Transaction tx = new Transaction();                // ④ 参数翻译:JSON 风格的
        tx.setBizNo(orderId);                             // orderId → bizNo
        tx.setAmountInCents(amount.multiply(BigDecimal.valueOf(100)).longValue());  // 元→分
        Response r = sdk.doTransaction(tx);               // 转发到新 SDK
        return translate(r);                              // 返回值翻译
    }
}

// ⑤ 调用方:一行不动——换 SDK 只需注入不同的 Adapter 实例
Payment payment = new NewBankPaymentAdapter(new NewBankSDK());
payment.pay("ORD001", new BigDecimal("99.99"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

三句话记住:实现 Target → 组合 Adaptee → 方法内翻译。调用方只面向接口,完全不知道底层 SDK 换了。

# 3.2 一次调用的完整时序

sequenceDiagram
    participant C as Client (Checkout)
    participant A as Adapter (NewBankPaymentAdapter)
    participant S as Adaptee (NewBankSDK)
    C->>A: pay(orderId="ORD001", amount=99.99)
    Note over A: 翻译参数:<br/>orderId → bizNo / 元→分
    A->>S: doTransaction(tx)
    S-->>A: Response{code=0, ...}
    Note over A: 翻译返回值:<br/>Response → PayResult
    A-->>C: PayResult{success}
1
2
3
4
5
6
7
8
9
10

Client 从头到尾只认识 Payment.pay()——Adapter 内部做了两次翻译(参数 + 返回值),Client 毫不知情。

# 3.3 适配器模式定义

将一个接口(Adaptee)转换成客户期望的另一个接口(Target),让原本接口不兼容的类可以一起工作。本质:调用方与三方接口之间的一层薄薄的"协议翻译层"。

# 3.4 典型使用场景

  • 替换三方 SDK:老支付→新支付、老日志→新日志——Adapter 翻译参数和返回值格式,业务层零改动;
  • 统一多供应商接口:敏感词过滤 A/B/C 三家接口各不同→三个 Adapter 统一成 filter(text),策略模式搭配使用;
  • 兼容老版本 API:JDK 中 Enumeration→Iterator 迁移——Collections.enumeration() 就是适配器;
  • 数据格式转换:Arrays.asList() 把数组适配成 List 接口;InputStreamReader 字节流转字符流。

反面提醒:设计阶段能统一接口就别事后用 Adapter——它是"补偿模式",不是"设计目标"。

# 3.5 三角色与结构图

flowchart LR
    C[Client<br/>调用方] -->|payment.pay| T[Target<br/>Payment 接口]
    T -->|委派| A[Adapter<br/>NewBankPaymentAdapter]
    A -->|doTransaction| AE[Adaptee<br/>NewBankSDK]
    style A fill:#e6f3ff
    style AE fill:#fdf6e3
1
2
3
4
5
6
角色 职责 示例
Target(目标接口) 调用方期望的接口 Payment 接口
Adaptee(被适配者) 需要被适配的原有类/三方 SDK NewBankSDK
Adapter(适配器) 实现 Target、组合/继承 Adaptee、做参数翻译 NewBankPaymentAdapter
Client(客户端) 通过 Target 接口调用 Checkout、OrderService

# 04.两种实现对比

适配器只有两种实现方式:类适配器(继承) 和 对象适配器(组合)——差异全在 Adapter 怎么拿到 Adaptee 的能力。

# 4.1 类适配器——继承 Adaptee

完整案例——电脑只能读 SD 卡,要读 TF 卡,用类适配器转换:

// Target:Computer 期望的接口
public interface SDCard { String readSD(); void writeSD(String msg); }
public class Computer {
    public String readSD(SDCard sd) { return sd.readSD(); } // 只认 SDCard
}

// Adaptee:TF 卡——Computer 不认识
public class TFCardImpl implements TFCard {
    public String readTF() { return "TF data"; }
    public void writeTF(String msg) { }
}

// Adapter:继承 TFCardImpl + 实现 SDCard——把 TF 翻译成 SD
public class SDAdapterTF extends TFCardImpl implements SDCard {
    public String readSD() {
        System.out.println("adapter read tf card");
        return readTF();                // 直接继承的方法——不用组合
    }
    public void writeSD(String msg) { writeTF(msg); }
}

// ✅ 使用:Computer 以为自己拿到的是 SD 卡
Computer computer = new Computer();
computer.readSD(new SDAdapterTF());    // 输出:"adapter read tf card" + "TF data"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

特点:Adaptee 的部分方法无需覆写就直接可用;受限于 Java 单继承,只能适配一个 Adaptee。

# 4.2 对象适配器——组合 Adaptee

完整案例——MediaPlayer 只能播 mp3,要播 vlc/mp4,用对象适配器扩展:

// Target:播放器接口——只认 play(audioType, filename)
public interface MediaPlayer { void play(String audioType, String fileName); }

// Adaptee:高级播放器——vlc / mp4 各不同
public interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}
public class VlcPlayer implements AdvancedMediaPlayer {
    public void playVlc(String f) { System.out.println("Playing vlc: " + f); }
    public void playMp4(String f) { /* do nothing */ }
}
public class Mp4Player implements AdvancedMediaPlayer {
    public void playVlc(String f) { /* do nothing */ }
    public void playMp4(String f) { System.out.println("Playing mp4: " + f); }
}

// Adapter:组合 AdvancedMediaPlayer——根据类型选择 vlc/mp4
public class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedPlayer;     // 组合,而非继承

    public MediaAdapter(String audioType) {
        if ("vlc".equals(audioType)) advancedPlayer = new VlcPlayer();
        else advancedPlayer = new Mp4Player();
    }
    public void play(String audioType, String fileName) {
        if ("vlc".equals(audioType)) advancedPlayer.playVlc(fileName);
        else advancedPlayer.playMp4(fileName);
    }
}

// ✅ 使用:MediaPlayer 不需要改一行——Adapter 接管了新格式
AudioPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3");               // 老格式直接播
player.play("mp4", "video.mp4");              // 新格式走 MediaAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

特点:灵活——可适配多个 Adaptee;"组合优于继承"原则的天然实践。

# 4.3 两种实现速查

维度 类适配器 对象适配器
实现方式 extends Adaptee + implements Target implements Target + 组合 Adaptee
适配数量 只能适配 1 个 Adaptee 可适配多个 Adaptee
复写 Adaptee 行为 ✅ 可覆写父类方法 ❌ 无法覆写——只能委托翻译
Java 多继承限制 ❌ 受限于单继承 ✅ 无限制
推荐度 ⭐⭐⭐(仅简单场景) ⭐⭐⭐⭐⭐(工程首选)

📌 一句话决策:Adaptee ≤ 1 个且接口契合 → 类适配器;其余所有场景 → 对象适配器。

# 4.4 默认适配器(缺省适配器 Pattern)

当一个接口有 10 个方法但只需要实现其中 2 个时,先写一个抽象适配器给所有方法空实现,子类按需覆写。这是一种"减少接口实现量"的工程手段:

// 原始接口——10 个方法
public interface MouseListener {
    void onClick(); void onDoubleClick(); void onDrag();
    void onHover();  void onEnter();   void onExit();
    void onMove();   void onWheel();   void onRightClick();
    void onLongPress();
}

// 默认适配器——全部空实现
public abstract class MouseAdapter implements MouseListener {
    public void onClick() { } public void onDoubleClick() { }
    public void onDrag() { }  public void onHover() { }
    public void onEnter() { } public void onExit() { }
    public void onMove() { }  public void onWheel() { }
    public void onRightClick() { } public void onLongPress() { }
}

// 子类只覆写需要的
new MouseAdapter() {
    public void onClick() { System.out.println("clicked!"); }
    public void onDrag()  { System.out.println("dragging..."); }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Android SDK 中的 XxxAdapter 和 Java Swing 的 XxxAdapter 几乎全是这种模式——接口十几个方法、业务只需其中两个,完美匹配缺省适配器的天选之子。

# 05.用前用后效果对比

为什么单独留一节做对比:用 01 节的支付事故做基准,量化 Adapter 到底省了什么。

# 5.1 🧪 回到事故现场:NewBankPaymentAdapter 改造

// ❌ 事故现场:47 处直接调 NewBankSDK,每处自己写元→分 + orderId→bizNo
public class Checkout {
    public void checkout(Order o) {
        Transaction tx = new Transaction();
        tx.setBizNo(o.getId());          // 必须记 47 处——忘改一处就是 bug
        tx.setAmountInCents(o.getAmount().multiply(new BigDecimal(100)).longValue());
        new NewBankSDK().doTransaction(tx);
    }
}

// ✅ Adapter 改造后:Checkout 一行不动
public class Checkout {
    private Payment payment;      // 还是面向老接口
    public void checkout(Order o) {
        payment.pay(o.getId(), o.getAmount());  // 一行不动
    }
}
// 换 SDK 只需注入不同的 Adapter:
// payment = new NewBankPaymentAdapter(new NewBankSDK());
// payment = new OldBankPaymentAdapter(new OldBankSDK());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

📊 改造效果量化:

指标 事故现场(全局替换) Adapter 改造后
业务代码改动量 47 处分散修改 0 处
新增文件 0(但 47 处重复转换逻辑) 1 个 NewBankPaymentAdapter
元→分转换写错概率 ⚠️ 47 处复制粘贴→必然发生 ✅ 集中 1 处→不可能
灰度切换新老 SDK 47 处插 if-else 注入不同 Adapter
回滚老 SDK 再全局搜索替换一次 注入老 Adapter
单元测试 无法 Mock SDK→只能集成测试 Mock Payment 接口→纯单测

结论:47 处改动 → 0 处改动 + 1 个 Adapter 文件。每次换 SDK 的动作从"全项目回归"蜕变成"加一个文件"。

# 5.2 🧪 统一多供应商:敏感词过滤改造

// ❌ 用前:三个供应商各调各的——参数各不相同
public class RiskManagement {
    private AFilter a = new AFilter();  // filterSexyWords + filterPoliticalWords
    private BFilter b = new BFilter();  // filter(text)
    private CFilter c = new CFilter();  // filter(text, mask)
    public String check(String text) {
        text = a.filterSexyWords(text);
        text = a.filterPoliticalWords(text);
        text = b.filter(text);
        text = c.filter(text, "***");   // 每加一个供应商都要改这里
        return text;
    }
}

// ✅ 用后:统一接口 + 各写一个 Adapter + 策略模式批量调用
public interface ISensitiveWordsFilter { String filter(String text); }

public class AAdapter implements ISensitiveWordsFilter {
    private AFilter a;
    public String filter(String text) {
        text = a.filterSexyWords(text);
        return a.filterPoliticalWords(text);
    }
}

public class RiskManagement {
    private List<ISensitiveWordsFilter> filters = new ArrayList<>();
    public String check(String text) {
        for (ISensitiveWordsFilter f : filters) text = f.filter(text);
        return text;   // 新增供应商只加 Adapter——RiskManagement 一行不动
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 5.3 生产场景速查

以下六大场景是适配器在生产环境中的主战场:

场景 说明 典型代码
封装有缺陷的接口 外部 SDK 全静态方法、命名反人类→Adapter 包一层改名 CDAdaptor extends CD implements ITarget
统一多个类的接口 敏感词过滤 A/B/C 三家各不同→统一 filter(text) 5.2 实例
替换外部系统 老支付换新支付——Adapter 翻译参数 5.1 实例
兼容老版本 Enumeration→Iterator,JDK 靠 Adapter 兼容 见下文 5.4
数据格式转换 Arrays.asList() 数组→List List<String> = Arrays.asList(a,b,c)
反向适配(防腐层) 三方接口常变→做一个厚 Adapter 层隔离 Anti-Corruption Layer 模式

# 5.4 经典案例:Enumeration→Iterator 的 JDK 适配

java.util.Collections.enumeration(Collection c) 就是适配器——把新 API Iterator 翻译成老 API Enumeration:

// JDK 源码简化版
public static Enumeration enumeration(final Collection c) {
    return new Enumeration() {
        private final Iterator i = c.iterator();    // 组合 Iterator
        public boolean hasMoreElements() { return i.hasNext(); }   // 方法名翻译
        public Object nextElement()     { return i.next(); }
    };
}

// 使用:老代码继续用 Enumeration,内部走的是 Iterator
for (Enumeration e = Collections.enumeration(list); e.hasMoreElements(); ) {
    System.out.println(e.nextElement());
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 5.5 封装有缺陷的第三方接口

外部 SDK 全静态方法、命名反人类、参数冗长——Adapter 包一层改名+收参数:

// ❌ 原始三方 SDK——丑、全是 static、参数多
public class LegacySDK {
    public static void s1() { }
    public void uglyName_x_2() { }
    public void tooManyParams(int a, int b, int c, int d) { }
}

// ✅ Adapter 改造——漂亮命名 + 参数封装
public class CleanAdapter implements NewInterface {
    private LegacySDK sdk;
    public void doTask()         { LegacySDK.s1(); }           // static→实例方法
    public void process()        { sdk.uglyName_x_2(); }       // 重命名
    public void execute(Config c){ sdk.tooManyParams(c.a(), c.b(), c.c(), c.d()); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 5.6 核心收益

🔑 核心收益:适配器模式把"外部接口的协议翻译"从业务代码里彻底隔离——调用方永远只面向自己定义的接口,三方 SDK 换了只加一个 Adapter 文件。它是项目对外部世界的"防御工事"——不是过度设计,是工程最低保障。

# 06.反面踩坑实录

为什么有这一节:Adapter 写起来"看似把 A 变成 B",但下面 4 类问题几乎人人翻过车。

# 6.1 🚨 静默吞掉语义——错误码全映射成"失败"

class NewBankPaymentAdapter implements Payment {
    public PayResult pay(String orderId, BigDecimal amount) {
        Response r = sdk.doTransaction(buildTx(orderId, amount));
        if (r.getCode() != 0) return PayResult.fail();  // ❌ 静默吞掉错误码
        return PayResult.success();
    }
}
// "已受理但未确认"、"需要重试"等中间态——全被映射成"失败"→业务层无法幂等
1
2
3
4
5
6
7
8

📌 教训:Adapter 是"接口转换",不是"语义改写"。保留并精确映射所有错误码。 ✅ 正解:new PayResult(r.getCode(), r.getMsg(), translateStatus(r.getStatus()))。

# 6.2 🚨 类适配器 + 多继承陷阱

// ❌ 需要同时适配 TFCard + OldCard —— 类适配器卡死
class SDAdapter extends TFCardImpl extends OldCardImpl implements SDCard {}  // Java 不支持
1
2

📌 教训:超过一个 Adaptee → 立刻换对象适配器。单继承语言的硬伤。

# 6.3 🚨 双向适配只接单向——回调丢失

// 正向:business → Adapter → NewSDK  ✅
// 反向:NewSDK 的异步回调 → ??? → business  ❌ Adapter 没接回调→支付成功但业务收不到通知
1
2

📌 教训:异步 SDK 需写双向适配器——正向 XxxAdapter + 反向 XxxCallbackAdapter,或一个类同时实现两边接口。

# 6.4 🚨 Adapter 里 new 临时对象→GC 抖动

public PayResult pay(String orderId, BigDecimal amount) {
    Transaction tx = new Transaction();   // 每次 new——高频调用下 Young GC 飙升
    SignBuilder sb = new SignBuilder();   // 无状态对象完全可复用
}
1
2
3
4

📌 教训:无状态对象用 static final 单例;有状态用 ThreadLocal。

# 6.5 踩坑速查 + 替代方案

坑 根因 正解
语义改写 错误码全映射失败 精确映射所有状态码
多继承 类适配器单继承受限 换对象适配器
回调丢失 只接正向不接反向 双向适配器
GC 抖动 频繁 new 临时对象 static final / ThreadLocal
你的场景 推荐
替换三方 SDK ✅ 对象适配器 + DI 注入
统一多供应商接口 ✅ 各写一个 Adapter 实现同一 Target
设计期可统一接口 ❌ 别用 Adapter——直接统一设计
两个接口语义完全不同 ❌ Adapter 不管语义翻译

# 07.决策树与选型

# 7.1 该不该用适配器

flowchart TD
    Start([接口不兼容需要对接]) --> Q1{能修改任意一方的接口吗?}
    Q1 -->|能| Redesign[✅ 直接统一接口<br/>设计期就该对齐]
    Q1 -->|不能——三方SDK / 老系统| Q2{有几个 Adaptee?}
    Q2 -->|1 个| Q3{接口契合度 > 70%?}
    Q2 -->|多个| Object[✅ 对象适配器<br/>组合 + 委托]
    Q3 -->|是| Class[✅ 类适配器<br/>继承—代码量少]
    Q3 -->|否| Object

    style Redesign fill:#dfd
    style Object fill:#e6f3ff
    style Class fill:#fff4e6
1
2
3
4
5
6
7
8
9
10
11
12

# 7.2 选型清单速查

场景 该用吗 推荐
替换第三方 SDK ✅ 该用 对象适配器 + DI 注入
统一多供应商接口 ✅ 该用 各写 Adapter 实现同一 Target
兼容老版本 API ✅ 该用 对象适配器
设计期可统一接口 ❌ 别用 直接统一设计
两边接口语义完全不同 ❌ 别用 改设计——Adapter 只管翻译不管语义
单次临时调用 ❌ 别用 写工具方法即可

# 08.总结与延伸

# 8.1 演化逻辑沉淀

阶段 核心问题 发现
01 支付事故 47 处改 SDK 调用→全链路崩 业务代码不应直接依赖三方接口
02 两次失败 全局搜索替换 / if-else 网关 转换散落各处→复制粘贴必翻车;路由和翻译耦合→SRP 违背
03 标准骨架 正确的姿势是什么? 实现 Target + 组合 Adaptee + 方法内翻译→三句话
04 两种实现 类 vs 对象适配器 类=继承(单 Adaptee)、对象=组合(多 Adaptee)→工程首选对象
05 效果对比 Adapter 到底省了什么? 47→0 处改动、一组文件代码一稿定
06 4 坑踩坑 语义改写/多继承/回调丢失/GC Adapter 只管翻译、别吞语义、双向适配、复用无状态对象
07 决策选型 什么时候用、什么时候不用? 不改双方源码时才用 Adapter;设计期能统一就别用

🔑 一句话核心:

适配器模式是"事后补救"的隔离层——业务层面向自己的接口永远不变,三方 SDK 的变化全被 Adapter 吸收。 它不是模式炫技,是工程对不确定性的最低防御。

# 8.2 四模式结构辨析——代理/装饰器/桥接/适配器

代理、装饰器、桥接、适配器——这 4 种结构型模式代码骨架非常相似,面试最常问区别。一句话区分:看接口是否改变。

flowchart LR
    subgraph 适配器
        A_C[Client] -->|期望 IPayment| A_Adapter
        A_Adapter -->|doTransaction| A_Adaptee[NewBankSDK]
    end
    subgraph 代理
        B_C[Client] -->|同接口| B_Proxy
        B_Proxy -->|同接口| B_Real[RealService]
    end
    subgraph 装饰器
        C1[BufferedInputStream] -.包装.-> C2[FileInputStream]
    end
    style A_Adapter fill:#f96
    style B_Proxy fill:#9cf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
模式 接口是否改变 意图 JDK 例子
适配器 ❌ 不同接口 翻译——把 A 接口转成 B 接口 InputStreamReader
(字节流→字符流)
代理 ✅ 同一接口 控制访问——加鉴权/限流 Collections.synchronizedList()
(同接口加锁)
装饰器 ✅ 同一接口 叠加增强——多级嵌套加能力 new BufferedInputStream(
new FileInputStream(f))
桥接 设计期分离 抽象与实现独立变化 JDBC Driver ↔ Connection

# 8.3 真实开源代码中的适配器

适配器是 Java 生态里最朴素也最普及的模式——下面每个案例都值得对照源码读一遍:

出处 形态 它在适配什么
JDK InputStreamReader 对象适配器 字节流→字符流(含字符集解码,继承 Reader + 组合 InputStream)
JDK Arrays.asList(T...) 对象适配器 数组→List 接口(30 行代码,工业级范本)
JDK Collections.list(Enumeration) 双向适配器 老 Enumeration ↔ 新 Iterator——JDK 的向后兼容方案
Spring MVC HandlerAdapter 适配器大本营 6 种 Handler(@RequestMapping、HttpRequestHandler 等)统一成 handle(req,res)
SLF4J slf4j-log4j12 桥接式适配 SLF4J API → Log4j/JUL/Logback——日志框架的"通用翻译层"
Android RecyclerView.Adapter 数据适配 业务数据 → ViewHolder 视图

学习建议:先看 Arrays.asList(30 行,最简单)→ 再看 InputStreamReader(含字符集解码,60 行)→ 最后读 Spring HandlerAdapter 体系(6 种实现互为参照)。三者读完,你就彻底理解适配器在工业系统中的核心地位。

# 8.4 ⚠️ 什么时候不该用适配器

  • 接口本来就该一致:如果你掌控两边接口的设计权,应在设计阶段统一,不要用 Adapter 弥补设计偷懒;
  • 两个接口语义完全不同:Adapter 只能"转换形式",不能"翻译语义"——把"加密"接口 Adapt 成"压缩"接口是写 Bug;
  • 只是单次性调用:临时一两次调用直接写工具方法,引入 Adapter 反而过度设计;
  • 应该用桥接的场合:如果"接口端"和"实现端"都要独立扩展→桥接模式,不是适配器;
  • 频繁 API 变更:三方 SDK 半年改 5 次接口签名→Adapter 也要跟着改 5 次→防不住,应该用防腐层(Anti-Corruption Layer)。

一句话:适配器是事后的补救,不是事前的炫技。设计阶段能统一就统一,统一不了再用 Adapter 兜底。

# 8.5 思考题

  1. 适配器和代理都有"包装目标对象"的结构——如何从"接口是否改变"上一句话区分?
  2. "类适配器 + 单继承限制"是 Java 才有的问题吗?Go/Python 里有没有同样的困扰?
  3. 如果三方 SDK 经常升级导致 Adapter 每月跟着改——这种场景下 Adapter 还是最佳选择吗?有没有更厚的防御层?
  4. Spring MVC 的 HandlerAdapter 实现了 6 个适配器——它怎么做到的"来一个 Handler 就自动找到对应的 Adapter"?这不就是"适配器的策略模式"吗?

上一篇 06.动态代理 → 本篇 → 08.装饰器:结构型和 Adapter 极易混淆——它不转换接口,而是给同一接口"叠加能力"。

上次更新: 2026/06/17, 11:43:57
动态代理设计模式
装饰者模式设计思想

← 动态代理设计模式 装饰者模式设计思想→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式