你有没有遇到过这种场面:后端接口返回了一个巨大的 JSON,字段名跟你的组件 props 完全对不上,你只好在每个组件里写一堆 data.user_info.bindPhone → phone 的映射代码。
改了一次还行,改第三次的时候你一定会想——这些转换逻辑,到底该写在哪?
这就是 Adapter 模式真正想回答的问题。
一、先看一个经典困境
假设你在做一个股票监控应用,数据源返回的是 XML。某天你引入了一个很强的分析库,但它只接受 JSON。
适配器问题示意:XML 数据源与 JSON 分析库无法直接对接
两个选择摆在面前:
• A:改分析库的源码,让它支持 XML → 你改不了(第三方库)
• B:在应用里到处写 XML→JSON 转换 → 转换逻辑散落各处,维护噩梦
这两条路都不对。问题不在于"怎么转",在于"谁来转,放在哪"。
二、Adapter 的核心思路
答案是:加一个中间层,让它专门负责"翻译"。
适配器解决方案:在调用方和第三方之间插入适配器层
Adapter 的做法很简单——创建一个新对象,它对外暴露调用方熟悉的接口,对内包装第三方服务。调用方不需要知道背后是 XML 还是 JSON,它只跟适配器打交道。
用原文那个经典的电源插头类比:你去欧洲旅行,美式插头插不进德国插座。你不会去改插座标准,也不会去改笔记本的电源线。你买一个转接头——它一端是美标,一端是欧标。
电源适配器类比:转接头两端分别适配不同标准
转接头不创造电能,它只负责让两套标准能对话。
三、结构一图看懂
对象适配器结构图:Client → ClientInterface → Adapter → Service
| 角色 | 职责 | 前端类比 |
|---|---|---|
| Client | 业务代码,只认自己的接口 | 你的 React 组件 |
| Client Interface | 业务代码期望的协议 | 组件的 Props 类型定义 |
| Service | 第三方服务,接口跟你不一样 | 后端 API / 第三方图表库 |
| Adapter | 实现 Client Interface,内部调用 Service | BFF 层 / 数据转换 hook |
关键点:Client 不跟 Adapter 的具体实现耦合,而是跟 Client Interface 耦合。这意味着——换一个第三方库,你只需要换一个 Adapter,业务代码一行不动。
这不就是软件工程里反复强调的"依赖抽象,不依赖具体"吗?Adapter 把这个原则从理论变成了一个可操作的结构。
四、前端的四个典型适配场景
很多前端同学觉得设计模式是后端的事。但 Adapter 在前端几乎无处不在,只是你可能没意识到自己天天在用。
1. BFF 返回数据整形
后端返回 { user_info: { bindPhone: "138xxx" } },组件需要 { phone: "138xxx" }。
// ❌ 在每个组件里手动映射
const phone = data.user_info.bindPhone
// ✅ 写一个 Adapter 函数,统一转换
function adaptUserResponse(raw: ApiUser): UserVO {
return {
phone: raw.user_info.bindPhone,
name: raw.user_info.nickName,
avatar: raw.user_info.avatarUrl,
}
}
所有组件只消费 UserVO,后端改了字段名?只改 adaptUserResponse 一个地方。
数据从哪来是后端的事,数据长什么样是适配层的事,组件只管用。
2. 图表库替换
从 ECharts 换到 AntV?如果你的业务代码直接调 echarts.init(),那每个页面都要改。
// ✅ 抽一层 ChartAdapter
interface ChartAdapter {
render(data: ChartData): void
destroy(): void
}
// ECharts 适配器
class EChartsAdapter implements ChartAdapter {
render(data: ChartData) {
// 把统一数据格式转成 ECharts option
}
}
// AntV 适配器——切换时只改这一个类
class AntVAdapter implements ChartAdapter {
render(data: ChartData) {
// 把统一数据格式转成 AntV spec
}
}
切换图表库 = 换一个 Adapter 实现,业务代码零改动。
3. 埋点 SDK 包装
今天用神策,明天用自研。直接调 sensors.track() 散落 50 个文件?
// ✅ 统一埋点适配层
const tracker: TrackAdapter = {
track(event: string, params: Record<string, unknown>) {
// 当前用神策
sensors.track(event, params)
// 切自研时只改这里
}
}
4. 组件库迁移
从 Ant Design 迁到内部 UI 库,最痛的是什么?不是样式不同,而是 API 不兼容。<Select onChange> 的回调参数格式不一样。这时候你需要的不是一口气改 200 个文件,而是先写一个适配层,让新旧 API 暂时共存。
// ✅ 适配 Select 组件的 onChange 差异
function AdaptedSelect(props: MySelectProps) {
return (
<InternalSelect
onChange={(val, option) => {
// 内部 UI 库返回 { value, label }
// 业务代码期望直接拿 value
props.onChange?.(val.value)
}}
/>
)
}
五、不只是"转格式"——Adapter 的深层价值
如果你只把 Adapter 理解成"转换数据格式",就矮化了它。
从系统论的角度看,Adapter 的本质是在系统边界设置缓冲区。生物学里有个类似的概念:细胞膜。细胞膜不是铁壁,但它精确控制了什么能进、什么能出、以什么形式进。没有细胞膜,细胞内部的精密反应会被外部环境直接干扰。
适配层就是你代码的"细胞膜"。
| 维度 | 没有适配层 | 有适配层 |
|---|---|---|
| 变更范围 | 第三方 API 改了 → 全项目改 | 第三方 API 改了 → 只改适配层 |
| 测试成本 | 业务逻辑和转换逻辑混在一起,难以单独测 | 适配层可独立单测 |
| 替换成本 | 换库 = 重写 | 换库 = 换 Adapter |
| 认知负担 | 每个开发者都要理解第三方 API 细节 | 只有适配层的维护者需要理解 |
从经济学的"比较优势"原则来看:每个模块应该专注做自己最擅长的事。业务组件擅长表达交互逻辑,不擅长处理第三方 API 的怪癖。让专业的层做专业的事,这才是真正的关注点分离。
六、什么时候不该用 Adapter
Adapter 不是银弹。原文也明确指出了它的代价:引入新的接口和类,增加了代码复杂度。
我的判断标准:
• ✅ 第三方依赖 + 未来可能替换 → 用
• ✅ 接口不兼容 + 改不了源码 → 用
• ❌ 内部代码 + 一次性使用 → 别用,过度设计
• ❌ 直接改源码更简单 → 那就直接改
好的架构不是到处加层,是在正确的边界加层。
七、Adapter 与它的"近亲"
最后理清几个容易混淆的模式:
| 模式 | 核心目的 | 接口变化 | 使用时机 |
|---|---|---|---|
| Adapter | 让不兼容的接口协作 | 转换接口 | 事后补救,已有系统集成 |
| Facade | 简化复杂子系统的使用 | 新建简化接口 | 子系统太复杂,需要统一入口 |
| Decorator | 在不改原接口的前提下增强功能 | 保持/扩展接口 | 需要动态叠加行为 |
| Proxy | 控制对原对象的访问 | 保持接口不变 | 需要加缓存、权限、懒加载 |
区分它们的关键一句话:Adapter 改接口,Decorator 加能力,Facade 降复杂度,Proxy 控访问。
如果你只想带走一句话,我建议记这个:
Adapter 真正解决的不是语法差异,而是把变化关在边界里,不让第三方的细节污染业务代码。
下次你在组件里写 data.xxx.yyy.zzz 的时候,停一秒想想——这个转换,是不是该放在别的地方。
参考原文:
• Refactoring.Guru — Adapter Design Pattern