08|适配器:把旧接口翻译成业务契约
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.adapter。
系列导航:Java 25 设计模式学习指南
文章发布服务希望调用 send(recipient, body),旧邮件组件却提供 deliver(content, address)。两个参数恰好都是 String,传反也能编译。若业务代码到处记住旧接口的顺序,未来替换组件时就要修改许多调用位置。
适配器把这种差异关在一个对象里。业务侧保留自己的通知语言,旧组件继续执行它原来的工作,中间对象负责准确翻译。
从问题提炼设计意图
适配器把已有对象的接口转换为调用方期望的接口,使不兼容的接口能够协作。本章使用持有旧对象引用的对象适配器。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
NotificationChannel |
目标接口,使用业务侧参数语义 |
LegacyMailer |
被适配者,模拟已有的旧接口 |
MailAdapter |
适配器,完成参数映射并委托 |
Demo |
只通过目标接口使用通知能力 |
classDiagram
NotificationChannel <|.. MailAdapter
MailAdapter --> LegacyMailer : delegates
Demo ..> NotificationChannel : uses
代码思路:变化应该落在哪个对象上
先定义调用方真正需要的 NotificationChannel,再让 MailAdapter 实现它。接口应表达稳定的业务能力,不能简单把第三方 SDK 所有方法改名后全部搬过来。
MailAdapter 的构造器注入 LegacyMailer,让依赖可见,也让旧组件的生命周期由装配方掌控。send 内先校验参数,再调用 deliver(body, recipient)。这一行参数顺序就是本例的语义转换点。
LegacyMailer 返回地址与内容拼接的回执,便于无网络地观察调用是否正确。测试用不同含义的字符串断言完整回执,能够发现参数传反;只断言调用没抛异常则无法发现这种适配错误。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.adapter,源码目录为 src/main/java/com/hanserwei/patterns/adapter/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
NotificationChannel.java:
package com.hanserwei.patterns.adapter;
/** 业务层使用的通知端口. */
public interface NotificationChannel {
/** 向收件人发送内容并返回发送回执. */
String send(String recipient, String body);
}
MailAdapter.java:
package com.hanserwei.patterns.adapter;
import java.util.Objects;
/** 把业务通知契约转换为旧邮件接口. */
public final class MailAdapter implements NotificationChannel {
/** 被适配的旧组件. */
private final LegacyMailer mailer;
/** 注入需要复用的旧组件. */
public MailAdapter(LegacyMailer mailer) {
this.mailer = Objects.requireNonNull(mailer, "mailer");
}
/** 校验参数并调整旧接口的参数顺序. */
@Override
public String send(String recipient, String body) {
Objects.requireNonNull(recipient, "recipient");
Objects.requireNonNull(body, "body");
return mailer.deliver(body, recipient);
}
}
LegacyMailer.java:
package com.hanserwei.patterns.adapter;
/** 无法修改的旧邮件接口替身,不访问真实网络. */
public final class LegacyMailer {
/** 旧接口先接收正文、再接收地址,返回模拟回执. */
public String deliver(String content, String address) {
return address + ":" + content;
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.adapter;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
NotificationChannel channel = new MailAdapter(new LegacyMailer());
System.out.println(channel.send("ada@example.test", "Published"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runAdapter
./gradlew test --tests 'com.hanserwei.patterns.adapter.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
ada@example.test:Published
用测试确认模式的行为
目标接口的 recipient 映射到旧接口 address,body 映射到 content;缺失旧组件时构造立即失败。
对应测试位于 src/test/java/com/hanserwei/patterns/adapter/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 封装第三方 SDK、历史组件或外部协议,使业务代码依赖本地接口。
- 在旧模型与新模型之间转换字段、单位和返回值。
- 替换供应商时,保持已有业务调用契约稳定。
适用边界与容易踩的坑
实际适配常常涉及错误码、时区、计量单位和异步完成语义,远不止方法改名。必须决定超时如何表达、失败是否可重试、是否保存原异常原因。不能把失败吞掉后返回“发送成功”,否则接口看似兼容,业务语义已经改变。
适配器不能创造旧系统没有的能力。如果目标接口承诺事务回滚,而旧系统只支持不可撤销发送,应缩小接口承诺或设计补偿流程,不能仅加一层类就声称满足契约。
本例返回模拟回执,不发送真实邮件。切换到真实 SDK 时,应让适配器负责协议翻译,把收件人授权、重试策略等职责放在明确的位置,避免堆成无边界的工具类。
与相近模式比较
桥接在设计阶段分离两个独立变化的维度;适配器通常是在已有接口不匹配时建立转换。装饰器保留接口并增强行为,适配器则重点改变调用方看到的接口。
动手练习
让 LegacyMailer 返回带状态码的结果,再把它映射为业务层的成功结果或领域异常。写出未知状态码测试,确保没有默认“成功”分支。说明哪些错误由适配器翻译,哪些重试应由上层决定。
系列导航:Java 25 设计模式学习指南