如何手写一个适配器模式?核心代码怎么设计?
简化版
手写适配器模式通常先定义客户端期望的 Target 接口,再创建已有的 Adaptee 类,最后写 Adapter 实现 Target,并在内部持有 Adaptee,把 Target 方法转换成 Adaptee 方法调用。工程中更推荐对象适配器,因为它基于组合,灵活且便于依赖注入。
详细版
核心代码可以按四步写:
- 定义目标接口;
- 准备已有但不兼容的类;
- 编写适配器实现目标接口;
- 客户端只依赖目标接口。
示例:
interface Target {
void request();
}
class Adaptee {
void specificRequest() {
System.out.println("已有接口");
}
}
class Adapter implements Target {
private final Adaptee adaptee;
Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
}
真实项目中,适配器不只是方法转发,还可能做参数转换、单位转换、异常转换、返回值包装和日志记录。但适配器不应该承载过多业务规则。
完整版教学
一、第一步:定义目标接口
目标接口应该从客户端需求出发。
比如系统内部希望统一发送通知:
interface Notifier {
NotifyResult send(NotifyCommand command);
}
这个接口是业务层希望看到的稳定抽象。
不要一开始就把第三方 SDK 的请求对象放进来,否则适配器还没写,系统就已经被第三方模型污染了。
二、第二步:已有类接口不兼容
假设第三方客户端是:
class ThirdPartyMessageClient {
ThirdPartyResponse push(String mobile, String text, String sign) {
// 调用第三方接口
return new ThirdPartyResponse();
}
}
它能发送消息,但方法名、参数形式和返回值都不符合 Notifier。
这就是适配器发挥作用的地方。
三、第三步:编写对象适配器
class ThirdPartyNotifierAdapter implements Notifier {
private final ThirdPartyMessageClient client;
private final String sign;
ThirdPartyNotifierAdapter(ThirdPartyMessageClient client, String sign) {
this.client = client;
this.sign = sign;
}
public NotifyResult send(NotifyCommand command) {
ThirdPartyResponse response = client.push(
command.getPhone(),
command.getContent(),
sign
);
return convert(response);
}
private NotifyResult convert(ThirdPartyResponse response) {
return new NotifyResult(response.isSuccess(), response.getMessage());
}
}
这里适配器做了三件事:
- 把内部命令转换成第三方参数;
- 调用第三方客户端;
- 把第三方响应转换成内部结果。
四、第四步:客户端只依赖目标接口
业务层代码应该是:
class OrderService {
private final Notifier notifier;
void createOrder() {
// 创建订单
notifier.send(command);
}
}
业务层不知道底层是阿里云、腾讯云还是其他供应商。这就是适配器带来的解耦效果。
五、适配器里可以做转换,但不要做核心业务
适配器适合做:
- 参数名称转换;
- 数据类型转换;
- 单位转换;
- 返回值包装;
- 异常转换;
- 外部错误码映射。
不适合做:
- 订单状态流转;
- 价格计算;
- 库存扣减;
- 复杂审批;
- 领域规则判断。
这些核心业务应该留在业务层或领域层。
六、对象适配器为什么更适合工程
对象适配器可以配合依赖注入:
@Component
class ThirdPartyNotifierAdapter implements Notifier {
private final ThirdPartyMessageClient client;
}
测试时也可以传入 mock client。相比继承式类适配器,组合方式更灵活,耦合更低。
七、转换的可逆性与测试证据
内部时间使用 UTC 毫秒 1720000000000,供应商要求秒,则应整除 1000;回转后最多损失 999 ms 精度,必须确认契约允许。
DomainRequest -> map units/time/null -> VendorRequest -> call -> map response/error
适配器的验收标准不是“调用成功”,而是 Target 的业务语义在转换前后保持成立。参数、单位、时区、精度、空值、错误码和资源所有权都应逐项形成契约样例,否则最危险的错误会以“成功响应”的形式潜伏。
八、语义边界与契约样例验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 适配器测试至少要覆盖正常映射、边界值、外部错误和资源释放。 |
| 适用边界 | 转换函数应集中、可测试并处理溢出、精度和 null;网络超时与重试不要随意藏在字段映射代码里。 |
| 测试证据 | 注入 Adaptee 测试替身,断言外部请求、返回映射、异常转换和调用次数 |
| 工程代价 | 重点统计字段映射与对象分配成本、外部模型变更影响面,以及多层版本适配造成的认知债务 |
易错点:适配器测试至少要覆盖正常映射、边界值、外部错误和资源释放。
九、常见误区与追问
- 误区:字段一一复制就是可靠适配。 同名字段可能单位、时区和空值语义不同,必须按契约逐项确认。
- 误区:适配器只需要让方法签名能够编译。 真正的适配还包括单位、时区、错误码、空值、资源释放和幂等语义;签名一致不代表行为兼容。
- 追问:如何测试适配器而不调用真实供应商? 注入 Adaptee 测试替身,断言传入外部请求和映射回的结果、异常及调用次数。
- 追问:适配器应该放在哪一层? 通常放在系统边界或防腐层,让业务层只依赖 Target,避免第三方模型和异常向内扩散。
- 追问:适配器测试为什么需要契约样例? 因为类型相同仍可能单位、精度或错误语义不同;契约样例能证明转换后的行为而不只是编译通过。
十、加强记忆
手写适配器记住四步:定义 Target,拿到 Adaptee,Adapter 实现 Target 并持有 Adaptee,Client 只依赖 Target。代码看似简单,真正的工程重点是把参数、返回值、异常和外部模型都挡在适配器内部。