适配器模式有哪些角色?调用流程是什么?
简化版
适配器模式通常包含 Client、Target、Adapter 和 Adaptee 四个角色。Client 面向 Target 编程,Adapter 实现 Target,内部调用 Adaptee,把客户端请求转换成被适配对象能理解的调用。
详细版
适配器模式的角色包括:
- Client:客户端,只依赖目标接口;
- Target:目标接口,也就是客户端希望使用的接口;
- Adaptee:被适配者,已有能力所在,但接口不兼容;
- Adapter:适配器,实现 Target,并把调用委托给 Adaptee。
调用流程是:
Client 调用 Target 方法
-> 实际执行 Adapter
-> Adapter 做参数、方法、返回值转换
-> Adapter 调用 Adaptee
-> 结果返回给 Client
面试时要强调:客户端不应该直接依赖 Adaptee,否则适配器的隔离价值会下降。适配器的目标是让客户端继续按统一接口使用能力,把外部或历史接口差异封装在适配层。
完整版教学
一、Client:客户端只关心目标接口
客户端是使用能力的一方。好的设计里,客户端只面向系统内部统一接口编程。
比如业务代码只认识:
interface MessageSender {
void send(String phone, String content);
}
业务代码不应该到处写:
aliyunClient.sendSms(...);
tencentClient.sendMessage(...);
如果业务层直接依赖多个供应商接口,系统会很快被外部 SDK 绑住。
二、Target:目标接口定义系统希望的样子
Target 是客户端真正依赖的抽象。
它应该从本系统业务视角出发设计,而不是照搬某个第三方接口。
比如短信发送接口可以设计成:
interface SmsSender {
SendResult send(String phone, String templateCode, Map<String, String> params);
}
这个接口表达的是“系统要发送短信”,而不是“某个供应商 SDK 的方法长什么样”。
三、Adaptee:被适配者提供已有能力
Adaptee 是已经存在的对象。它可能是:
- 老系统类;
- 第三方 SDK;
- 遗留接口;
- 不同协议的客户端;
- 另一个模块暴露的服务。
它的问题不是没有能力,而是接口和 Target 不一致。
比如:
class AliyunSmsClient {
AliyunResponse sendTemplateSms(AliyunSmsRequest request) {
// 调阿里云短信
}
}
这个类能发送短信,但它不是 SmsSender。
四、Adapter:适配器连接两边
Adapter 实现系统目标接口,内部持有被适配者:
class AliyunSmsAdapter implements SmsSender {
private final AliyunSmsClient client;
public SendResult send(String phone, String templateCode, Map<String, String> params) {
AliyunSmsRequest request = new AliyunSmsRequest();
request.setPhone(phone);
request.setTemplateCode(templateCode);
request.setParams(params);
AliyunResponse response = client.sendTemplateSms(request);
return convert(response);
}
}
这里的关键不只是调用转发,还包括参数转换、返回值转换、异常转换和语义对齐。
五、调用流程要体现“客户端不知道被适配者”
完整调用关系应该是:
OrderService -> SmsSender -> AliyunSmsAdapter -> AliyunSmsClient
OrderService 不知道阿里云 SDK 的请求对象、响应对象、异常类型。以后换成腾讯云短信,只需要新增或替换适配器。
这就是适配器模式的隔离价值。
六、适配器里可以做哪些转换
适配器常见转换包括:
- 方法名转换;
- 参数格式转换;
- 单位转换,比如分和元;
- 枚举值转换;
- 返回值包装;
- 异常类型转换;
- 默认值填充;
- 协议对象转换。
但适配器不应该承担过多业务决策。复杂业务规则应该放在业务服务或领域层,而不是藏在适配层。
七、四个角色的数据和异常边界
Client 只传 1 个 DomainRequest,Adapter 将其拆成供应商 3 个字段,并把 7 种外部错误码归并为 3 类领域异常。
Client -> Target -> Adapter(map request/error) -> Adaptee
适配器的验收标准不是“调用成功”,而是 Target 的业务语义在转换前后保持成立。参数、单位、时区、精度、空值、错误码和资源所有权都应逐项形成契约样例,否则最危险的错误会以“成功响应”的形式潜伏。
八、语义边界与契约样例验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 客户端不知道 Adaptee,不代表适配层可以吞掉关键失败信息。 |
| 适用边界 | Adapter 可转换参数、返回值、异常和生命周期,但核心业务决策应留在领域服务。 |
| 测试证据 | 注入 Adaptee 测试替身,断言外部请求、返回映射、异常转换和调用次数 |
| 工程代价 | 重点统计字段映射与对象分配成本、外部模型变更影响面,以及多层版本适配造成的认知债务 |
易错点:客户端不知道 Adaptee,不代表适配层可以吞掉关键失败信息。
九、常见误区与追问
- 误区:Adaptee 必须实现 Target。 被适配者通常接口不兼容,正因为不实现 Target 才需要 Adapter 居中转换。
- 误区:适配器只需要让方法签名能够编译。 真正的适配还包括单位、时区、错误码、空值、资源释放和幂等语义;签名一致不代表行为兼容。
- 追问:异常应该原样抛给业务层吗? 应保留诊断原因并转换为稳定领域异常,避免业务代码依赖供应商异常类型。
- 追问:适配器应该放在哪一层? 通常放在系统边界或防腐层,让业务层只依赖 Target,避免第三方模型和异常向内扩散。
- 追问:适配器测试为什么需要契约样例? 因为类型相同仍可能单位、精度或错误语义不同;契约样例能证明转换后的行为而不只是编译通过。
十、加强记忆
适配器模式的角色可以记成“客户端看 Target,适配器接 Adaptee”。Client 不碰外部接口,Adapter 负责把本系统目标接口转换成被适配者调用。答题时顺着 Client、Target、Adapter、Adaptee 讲,基本就能把结构和价值讲清楚。