Adapter:变化该放在哪层?

0 阅读6分钟

你有没有遇到过这种场面:后端接口返回了一个巨大的 JSON,字段名跟你的组件 props 完全对不上,你只好在每个组件里写一堆 data.user_info.bindPhone → phone 的映射代码。

改了一次还行,改第三次的时候你一定会想——这些转换逻辑,到底该写在哪?

这就是 Adapter 模式真正想回答的问题。

一、先看一个经典困境

假设你在做一个股票监控应用,数据源返回的是 XML。某天你引入了一个很强的分析库,但它只接受 JSON。

适配器问题示意:XML 数据源与 JSON 分析库无法直接对接

适配器问题示意:XML 数据源与 JSON 分析库无法直接对接

两个选择摆在面前:

• A:改分析库的源码,让它支持 XML → 你改不了(第三方库)

• B:在应用里到处写 XML→JSON 转换 → 转换逻辑散落各处,维护噩梦

这两条路都不对。问题不在于"怎么转",在于"谁来转,放在哪"。

二、Adapter 的核心思路

答案是:加一个中间层,让它专门负责"翻译"。

适配器解决方案:在调用方和第三方之间插入适配器层

适配器解决方案:在调用方和第三方之间插入适配器层

Adapter 的做法很简单——创建一个新对象,它对外暴露调用方熟悉的接口,对内包装第三方服务。调用方不需要知道背后是 XML 还是 JSON,它只跟适配器打交道。

用原文那个经典的电源插头类比:你去欧洲旅行,美式插头插不进德国插座。你不会去改插座标准,也不会去改笔记本的电源线。你买一个转接头——它一端是美标,一端是欧标。

电源适配器类比:转接头两端分别适配不同标准

电源适配器类比:转接头两端分别适配不同标准

转接头不创造电能,它只负责让两套标准能对话。

三、结构一图看懂

对象适配器结构图:Client → ClientInterface → Adapter → Service

对象适配器结构图:Client → ClientInterface → Adapter → Service

角色职责前端类比
Client业务代码,只认自己的接口你的 React 组件
Client Interface业务代码期望的协议组件的 Props 类型定义
Service第三方服务,接口跟你不一样后端 API / 第三方图表库
Adapter实现 Client Interface,内部调用 ServiceBFF 层 / 数据转换 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

qrcode_for_gh_6a9e7f3719d6_344.jpg