讲解
出国旅行带个转换插头:墙上的插座形状没变,你的电器插头也没变,中间一个小小的转换头把两边接了起来。适配器模式干的就是转换头的活——把一个类的接口转换成客户端期望的另一个接口,让原本因为接口不兼容而不能合作的类一起工作。
这种需求在集成场景里铺天盖地:接第三方支付 SDK,它的方法叫 makePayment(分, 币种),你的系统约定的是 pay(元);升级日志库,老接口是 log(level, msg),新接口拆成了 info(msg)、error(msg)。改自己系统的所有调用点、或给第三方库包一层?答案几乎总是后者——把不兼容挡在一个薄薄的适配层里。
适配器有两种经典形态:类适配器靠多重继承(适配器同时继承目标接口和被适配者),在 TypeScript/Java 这类单继承语言里基本不用;对象适配器靠组合——适配器持有被适配者的引用,实现目标接口,在方法里把调用翻译过去。本教程和工业实践里说的适配器,默认都是对象适配器。
适配器的价值不止在「翻译参数」,还在于语义防腐。第三方 SDK 的概念模型(分、错误码、回调风格)被挡在适配器之外,你的领域代码继续用自己的语言(元、异常、Promise)说话。这也是为什么整洁架构里,适配器层是业务逻辑和外部世界之间标准的「防腐层」。
示例
把第三方支付 SDK 的「分 + 币种」老接口,适配成系统统一的「元」接口:
import assert from 'node:assert/strict';
// 第三方 SDK:方法名、单位、参数个数都和我们的约定不同
class LegacyPaySDK {
makePayment(amountInCents: number, currency: string): string {
return 'OLD-SDK 支付 ' + amountInCents + ' 分 (' + currency + ')';
}
}
// 我们系统期望的统一接口
interface PaymentGateway {
pay(yuan: number): string;
}
// 适配器:实现新接口,内部持有并调用老对象
class LegacyPayAdapter implements PaymentGateway {
constructor(private readonly sdk: LegacyPaySDK) {}
pay(yuan: number): string {
const cents = Math.round(yuan * 100); // 单位换算也封装在适配器里
return this.sdk.makePayment(cents, 'CNY');
}
}
// 系统其余部分只认识 PaymentGateway
function checkout(gateway: PaymentGateway, yuan: number): string {
return gateway.pay(yuan);
}
const result = checkout(new LegacyPayAdapter(new LegacyPaySDK()), 19.9);
assert.equal(result, 'OLD-SDK 支付 1990 分 (CNY)');
console.log(result);
注意适配发生在哪儿:元转分、币种固定为 CNY,这些「翻译知识」全部收在 LegacyPayAdapter 内部。checkout 函数对老 SDK 一无所知;将来换一家支付公司,再写一个 XxxPayAdapter 实现同一个 PaymentGateway 即可,checkout 和业务代码零改动。对象适配器用组合(构造函数注入 sdk)而不是继承,这也让测试时可以轻松换成 mock。
常见坑
- 适配器里长业务逻辑:适配器只做接口翻译和必要的单位/格式转换;折扣、限额这些规则属于领域层,混进来会让适配器变成无人敢动的泥球。
- 到处散落内联翻译:在调用点随手写 yuan * 100,十处调用十处翻译,比不用适配器更糟——翻译必须收进一个类。
- 双向适配器蔓延:一个类同时适配 A→B 和 B→A 两个方向,职责立刻模糊;两个方向就写两个适配器。
- 为「将来可能接入」提前建适配层:只有一个实现且接口稳定时,直接面向它编程即可;适配器是给「真实存在的不兼容」准备的。
- 忽略异常和错误码的适配:接口翻译不只方法签名——老 SDK 返回错误码、新接口抛异常,这层语义转换漏掉,上层就会被错误码污染。
小结
适配器用组合把一个接口翻译成另一个接口,隔离第三方/遗留系统的语义入侵,让业务代码始终说自己领域的语言。下一章的桥接模式同样是「两个维度分离」,但它面对的是抽象与实现两个都会变化的轴。