配套代码: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 设计模式学习指南

上一篇:单例 · 下一篇:桥接