设计模式实战解读(七):适配器模式——让不兼容的接口无缝协作

🔔 本文 5000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第七篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

适配器模式(Adapter):将一个类的接口转换成调用方期望的另一个接口,使原本接口不兼容的类可以一起工作。

归属:结构型模式。


一、没有适配器时的痛点

假设你在做一个统一消息通知系统,需要对接多个第三方通知渠道:

java 复制代码
public class NotificationService {

    public void notify(String userId, String content) {
        // 钉钉 SDK
        DingTalkClient dingClient = new DingTalkClient();
        dingClient.sendMarkdown(userId, "通知", content, false);

        // 企业微信 SDK
        WeCom wecom = new WeCom(corpId, secret);
        wecom.message().send(new TextMessage(userId, content));

        // 飞书 SDK
        LarkSender lark = LarkSender.getInstance();
        lark.pushText(LarkMsg.builder().openId(userId).text(content).build());
    }
}

三个 SDK 的接口完全不一样:

SDK 发送方法 参数形式 返回值
钉钉 sendMarkdown(userId, title, content, isAtAll) 4 个散参 void
企微 message().send(TextMessage) 链式调用 + 包装对象 SendResult
飞书 pushText(LarkMsg) Builder 对象 Response

问题:

  1. 调用方和具体 SDK 强耦合------换一个 SDK 版本,调用方要跟着改
  2. 无法统一处理------想加个"发送失败重试"的通用逻辑,要给三个 SDK 各写一遍
  3. 难以扩展------新加一个渠道(如 Slack),要在 NotificationService 里再加一坨代码
  4. 无法多态------三个 SDK 没有公共接口,无法用 List 统一遍历发送

这就是经典的"接口不兼容"问题------你的系统期望的是统一的发送接口,但第三方 SDK 各有各的设计。


二、模式结构

复制代码
┌───────────────────────────────────┐
│     Target(目标接口)              │
│  + send(userId, content): Result  │ ← 调用方期望的统一接口
└──────────────────┬────────────────┘
                   │ 实现
                   ↓
┌───────────────────────────────────┐
│     Adapter(适配器)              │
│  - adaptee: DingTalkClient       │ ← 持有被适配对象
│  + send(userId, content): Result │ ← 把调用转换成 adaptee 的格式
└───────────────────────────────────┘
                   │ 委托
                   ↓
┌───────────────────────────────────┐
│     Adaptee(被适配者)            │
│  + sendMarkdown(...)             │ ← 原始的不兼容接口
└───────────────────────────────────┘

三个角色:

  • Target(目标接口):调用方期望的标准接口
  • Adaptee(被适配者):已有的、接口不兼容的类(通常是第三方 SDK 或旧系统)
  • Adapter(适配器) :实现 Target 接口,内部持有 Adaptee,负责把 Target 的调用翻译成 Adaptee 能理解的调用

一句话:适配器就是"翻译官"------把外语翻译成你能听懂的话。


三、核心实现

3.1 对象适配器(推荐)

通过组合持有被适配对象:

java 复制代码
// 目标接口:统一的消息发送能力
public interface MessageNotifier {
    NotifyResult send(String userId, String content);
    String channel(); // 标识渠道名
}

// 适配器1:适配钉钉 SDK
@Component
public class DingTalkNotifierAdapter implements MessageNotifier {
    private final DingTalkClient dingTalkClient;

    public DingTalkNotifierAdapter(DingTalkClient dingTalkClient) {
        this.dingTalkClient = dingTalkClient;
    }

    @Override
    public String channel() {
        return "dingtalk";
    }

    @Override
    public NotifyResult send(String userId, String content) {
        try {
            // 将统一接口的参数"翻译"成钉钉SDK的参数格式
            dingTalkClient.sendMarkdown(userId, "系统通知", content, false);
            return NotifyResult.success();
        } catch (DingTalkException e) {
            return NotifyResult.fail(e.getErrCode(), e.getErrMsg());
        }
    }
}

// 适配器2:适配企业微信 SDK
@Component
public class WeComNotifierAdapter implements MessageNotifier {
    private final WeCom wecom;

    public WeComNotifierAdapter(WeCom wecom) {
        this.wecom = wecom;
    }

    @Override
    public String channel() {
        return "wecom";
    }

    @Override
    public NotifyResult send(String userId, String content) {
        try {
            SendResult result = wecom.message().send(new TextMessage(userId, content));
            return result.isSuccess()
                ? NotifyResult.success()
                : NotifyResult.fail(result.getErrCode(), result.getErrMsg());
        } catch (Exception e) {
            return NotifyResult.fail("WECOM_ERROR", e.getMessage());
        }
    }
}

// 适配器3:适配飞书 SDK
@Component
public class LarkNotifierAdapter implements MessageNotifier {
    private final LarkSender larkSender;

    public LarkNotifierAdapter(LarkSender larkSender) {
        this.larkSender = larkSender;
    }

    @Override
    public String channel() {
        return "lark";
    }

    @Override
    public NotifyResult send(String userId, String content) {
        try {
            LarkMsg msg = LarkMsg.builder().openId(userId).text(content).build();
            Response<Void> resp = larkSender.pushText(msg);
            return resp.isSuccess()
                ? NotifyResult.success()
                : NotifyResult.fail(String.valueOf(resp.getCode()), resp.getMsg());
        } catch (Exception e) {
            return NotifyResult.fail("LARK_ERROR", e.getMessage());
        }
    }
}

3.2 统一调用

java 复制代码
@Service
public class NotificationService {

    // Spring 自动注入所有适配器,构建渠道 Map
    private final Map<String, MessageNotifier> notifierMap;

    public NotificationService(List<MessageNotifier> notifiers) {
        this.notifierMap = notifiers.stream()
            .collect(Collectors.toMap(MessageNotifier::channel, Function.identity()));
    }

    public NotifyResult notify(String channel, String userId, String content) {
        MessageNotifier notifier = notifierMap.get(channel);
        if (notifier == null) {
            throw new BizException("Unsupported channel: " + channel);
        }
        return notifier.send(userId, content);
    }

    // 群发所有渠道
    public List<NotifyResult> notifyAll(String userId, String content) {
        return notifierMap.values().stream()
            .map(n -> n.send(userId, content))
            .collect(Collectors.toList());
    }
}

新增一个渠道(Slack),只需要加一个 SlackNotifierAdapter,实现 MessageNotifier 接口即可------Service 层零改动。

3.3 类适配器(了解即可)

通过继承被适配类,同时实现目标接口:

java 复制代码
// 类适配器:继承 Adaptee + 实现 Target(Java 只能单继承,有局限)
public class DingTalkNotifierClassAdapter extends DingTalkClient implements MessageNotifier {

    @Override
    public String channel() {
        return "dingtalk";
    }

    @Override
    public NotifyResult send(String userId, String content) {
        // 直接调用父类方法(继承来的)
        this.sendMarkdown(userId, "通知", content, false);
        return NotifyResult.success();
    }
}

实际项目中很少用类适配器------Java 单继承限制太大,且对象适配器更灵活。


四、真实应用场景

4.1 框架级应用

框架 适配器 被适配者 → 目标接口
Spring MVC HandlerAdapter 各种 Controller → 统一的处理器接口
SLF4J slf4j-log4j12 / logback-classic Log4j/Logback → SLF4J API
Spring Security UserDetailsService 任意用户存储 → Spring Security 用户体系
JDBC Driver 各数据库协议 → JDBC 标准接口
Jackson JsonSerializer / Deserializer 自定义类型 → JSON 序列化格式

4.2 SLF4J------最经典的适配器架构

复制代码
┌────────────┐     ┌──────────────────┐     ┌─────────────┐
│ 你的代码    │────→│  SLF4J API       │←────│ slf4j-api   │
│ log.info() │     │ (Target接口)     │     │ (接口JAR)   │
└────────────┘     └────────┬─────────┘     └─────────────┘
                            │ 适配
              ┌─────────────┼─────────────┐
              ↓             ↓             ↓
     logback-classic   slf4j-log4j12   slf4j-jdk14
     (Logback适配器)   (Log4j适配器)   (JUL适配器)
              │             │             │
              ↓             ↓             ↓
         Logback引擎    Log4j引擎     JUL引擎

你的代码只依赖 SLF4J 接口,底层切换日志框架只需要换一个适配器 JAR------适配器模式的教科书案例。

4.3 业务场景

场景 被适配者 适配为 动因
老系统对接 老 SOAP 接口 REST 接口 新系统只认 REST
多渠道支付 各支付 SDK 统一 PayChannel 接口不一致
数据导入 Excel/CSV/JSON 统一 DataRow 格式不同
消息推送 钉钉/企微/飞书 统一 Notifier 接口不兼容
云存储 阿里OSS/S3/MinIO 统一 FileStore SDK 不一致

4.4 iPaaS 连接器的适配器体系

在 iPaaS 平台中,每个连接器就是一个"适配器"------将千差万别的第三方 API 适配为平台统一的执行接口:

java 复制代码
// 平台统一的连接器执行接口(Target)
public interface ConnectorExecutor {
    ExecuteResult execute(ConnectorRequest request);
    String connectorType();
}

// 钉钉连接器适配器
@Component
public class DingTalkConnectorAdapter implements ConnectorExecutor {

    @Override
    public String connectorType() {
        return "dingtalk";
    }

    @Override
    public ExecuteResult execute(ConnectorRequest request) {
        // 1. 从统一请求中提取参数
        String action = request.getAction(); // "send_message" / "create_task"
        Map<String, Object> params = request.getParams();

        // 2. 翻译为钉钉 API 的格式
        DingTalkApiRequest dingReq = DingTalkRequestBuilder.build(action, params);

        // 3. 调用钉钉 API
        DingTalkApiResponse dingResp = dingTalkHttpClient.call(dingReq);

        // 4. 将钉钉响应翻译回统一格式
        return ExecuteResult.builder()
            .success(dingResp.getErrcode() == 0)
            .data(dingResp.getBody())
            .errorMsg(dingResp.getErrmsg())
            .build();
    }
}

// 企业微信连接器适配器
@Component
public class WeComConnectorAdapter implements ConnectorExecutor {

    @Override
    public String connectorType() {
        return "wecom";
    }

    @Override
    public ExecuteResult execute(ConnectorRequest request) {
        // 相同的翻译逻辑,但对接企微的 API 格式
        WeComRequest wecomReq = WeComRequestBuilder.build(
            request.getAction(), request.getParams()
        );
        WeComResponse wecomResp = wecomClient.invoke(wecomReq);
        return ExecuteResult.from(wecomResp);
    }
}

iPaaS 平台对接 600+ 应用,每个连接器本质上就是一个适配器------将各种不兼容的第三方 API 翻译为平台统一的 ConnectorRequest/ExecuteResult 协议


五、常见变种

5.1 双向适配器

同时实现两边的接口,可以在两个系统之间双向转换:

java 复制代码
// 既能把 NewLogger 当 OldLogger 用,也能把 OldLogger 当 NewLogger 用
public class LoggerBidirectionalAdapter implements OldLogger, NewLogger {
    private OldLogger oldLogger;
    private NewLogger newLogger;

    // 当作 OldLogger 使用时
    @Override
    public void writeLog(String msg) {
        if (newLogger != null) {
            newLogger.log(Level.INFO, msg, null);
        }
    }

    // 当作 NewLogger 使用时
    @Override
    public void log(Level level, String msg, Throwable t) {
        if (oldLogger != null) {
            oldLogger.writeLog("[" + level + "] " + msg);
        }
    }
}

适用场景:新老系统并行期间,需要双向兼容。

5.2 默认适配器(缺省适配器)

当接口方法太多、但只想实现其中几个时,用一个抽象类提供空实现:

java 复制代码
// 接口有 10 个方法
public interface EventListener {
    void onStart(Event e);
    void onPause(Event e);
    void onResume(Event e);
    void onStop(Event e);
    void onError(Event e);
    // ... 更多方法
}

// 默认适配器:所有方法空实现
public abstract class EventListenerAdapter implements EventListener {
    @Override public void onStart(Event e) {}
    @Override public void onPause(Event e) {}
    @Override public void onResume(Event e) {}
    @Override public void onStop(Event e) {}
    @Override public void onError(Event e) {}
}

// 使用时只覆写关心的方法
public class MyListener extends EventListenerAdapter {
    @Override
    public void onError(Event e) {
        alertService.send("发生异常: " + e.getMessage());
    }
}

Spring 的 WebMvcConfigurerAdapter(已废弃)就是这个思路,现在 Java 8 接口默认方法取代了它。

5.3 适配器 + 工厂

用工厂根据类型选择合适的适配器:

java 复制代码
@Component
public class NotifierAdapterFactory {
    private final Map<String, MessageNotifier> adapterMap;

    public NotifierAdapterFactory(List<MessageNotifier> adapters) {
        this.adapterMap = adapters.stream()
            .collect(Collectors.toMap(MessageNotifier::channel, Function.identity()));
    }

    public MessageNotifier getAdapter(String channel) {
        MessageNotifier adapter = adapterMap.get(channel);
        if (adapter == null) {
            throw new BizException("No adapter found for channel: " + channel);
        }
        return adapter;
    }
}

六、优缺点

优点 缺点
让不兼容的类协同工作 增加了一层间接性(适配层)
复用已有类,不修改其代码 适配器过多时,系统结构变复杂
符合单一职责(适配逻辑集中在 Adapter) 类适配器受 Java 单继承限制
符合开闭原则(新增适配器不改已有代码) 过度使用适配器可能是接口设计有问题的信号
解耦调用方和第三方 SDK 适配器本身可能变成"胖类"(转换逻辑复杂时)

七、避坑指南

坑 1:适配器内做业务逻辑

java 复制代码
// ❌ 适配器不应该包含业务逻辑
public class DingTalkNotifierAdapter implements MessageNotifier {
    @Override
    public NotifyResult send(String userId, String content) {
        // 这些是业务逻辑,不该放在适配器里!
        if (isNightTime()) {
            return NotifyResult.skip("夜间免打扰");
        }
        content = sensitiveWordFilter(content); // 敏感词过滤
        dingTalkClient.sendMarkdown(...);
    }
}

// ✓ 适配器只做接口转换,业务逻辑在 Service 层
public class DingTalkNotifierAdapter implements MessageNotifier {
    @Override
    public NotifyResult send(String userId, String content) {
        // 只做"翻译":统一参数 → 钉钉参数
        dingTalkClient.sendMarkdown(userId, "通知", content, false);
        return NotifyResult.success();
    }
}

原则:适配器只负责"格式转换",不做"业务决策"。

坑 2:异常没有统一转换

java 复制代码
// ❌ 适配器直接抛出 SDK 特有异常
public NotifyResult send(String userId, String content) {
    dingTalkClient.sendMarkdown(...); // 抛 DingTalkException
    // 调用方还要 catch DingTalkException,耦合没有解除
}

// ✓ 统一转换为平台异常
public NotifyResult send(String userId, String content) {
    try {
        dingTalkClient.sendMarkdown(...);
        return NotifyResult.success();
    } catch (DingTalkException e) {
        // 翻译异常,对外暴露统一的错误模型
        return NotifyResult.fail(e.getErrCode(), e.getErrMsg());
    }
}

坑 3:适配器数据丢失

第三方 API 返回了 10 个字段,但你的 Target 接口只定义了 3 个字段------其他 7 个信息就丢了。

解法 :Target 接口设计时预留 Map<String, Object> extra 扩展字段,或用泛型支持扩展。

java 复制代码
public class ExecuteResult {
    private boolean success;
    private Object data;
    private String errorMsg;
    private Map<String, Object> extra; // 预留扩展,放不进标准字段的信息
}

坑 4:适配器与 Adaptee 版本耦合

第三方 SDK 升级后方法签名变了,适配器编译失败。

解法

  • 适配器模块单独维护,不要散落在业务代码中
  • 适配器内加版本判断或抽取 SdkVersion 策略
  • 用 Maven/Gradle 模块隔离适配器依赖

八、常见问题(FAQ)

Q:适配器模式和装饰器模式都是"包一层",有什么区别?

A:核心意图不同:

  • 适配器 :接口不兼容,要把 A 接口转换成 B 接口------改变接口形态
  • 装饰器 :接口相同,要给现有能力添加新功能------增强同一个接口

适配器的 wrapped 对象和 Target 接口不同 ;装饰器的 wrapped 对象和自身相同接口

Q:适配器模式和外观模式(Facade)有什么区别?

A:

  • 适配器:一对一转换(一个 Adaptee 适配为一个 Target)
  • 外观:多对一封装(多个子系统组合为一个简洁接口)

外观模式侧重"简化复杂子系统的使用",适配器侧重"兼容不同接口"。

Q:什么时候该用适配器,什么时候该重新设计接口?

A:

  • 用适配器:Adaptee 是你无法修改的(第三方 SDK、旧系统、已发布的 API)
  • 重新设计:如果是自己团队的代码,且接口设计本身有问题,应该直接改接口,而不是堆适配器掩盖设计缺陷

Q:Spring MVC 的 HandlerAdapter 为什么叫适配器?

A:Spring MVC 支持多种 Controller 写法(@Controller 注解、实现 Controller 接口、实现 HttpRequestHandler 接口)。DispatcherServlet 需要一个统一的方式来调用它们------HandlerAdapter 就是适配器,把不同类型的 Controller 适配为统一的 handle(request, response, handler) 调用。

Q:一个适配器可以适配多个 Adaptee 吗?

A:可以,但不推荐。一个适配器适配多个 Adaptee 会导致适配器变成"胖类",违反单一职责。推荐一个 Adaptee 对应一个 Adapter。


九、小结

适配器模式的核心价值:让已有的、接口不兼容的代码能无缝接入你的系统,不修改任何一方的原始代码。

三个实践要点:

  1. 适配器只做"翻译",不做业务逻辑------格式转换、异常转换、字段映射,仅此而已
  2. 异常必须统一转换------不能让 SDK 特有异常穿透适配层泄露给调用方
  3. 优先用对象适配器(组合)而非类适配器(继承)------更灵活,不受单继承限制

下一篇我们聊代理模式------当你需要控制对一个对象的访问(权限校验、懒加载、远程调用、限流),而不想让调用方感知这层控制时,代理是最佳选择。


标签:#设计模式 #适配器模式 #Adapter #结构型模式 #Java #Spring #接口兼容 #第三方SDK #连接器适配 #SLF4J #iPaaS #面向对象 #软件工程

相关推荐
MacroZheng14 分钟前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
SamDeepThinking40 分钟前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
Zane199443 分钟前
接口都能写默认实现了,为什么还需要抽象类
java·后端
未秃头的程序猿1 小时前
读Spring AI源码的方法——不是硬啃,是带着问题去翻
java·后端·spring
用户3126874877201 小时前
别再 Thread.sleep 硬等了!并发工具类到底怎么选?
java
Java内核笔记1 小时前
万字长文剖析 Spring Boot 4 自动配置机制源码:从 @EnableAutoConfiguration 到条件装配
java·后端
evans在进步1 小时前
Spring MVC 核心机制详解:全局异常处理、请求跳转与数据绑定
java·spring·mvc
嘟嘟07171 小时前
TypeScript 工具类型通关:Pick、Omit、Partial、Record 区别与 Omit 等价实现一次讲清
设计模式·面试·typescript
weixin_BYSJ19871 小时前
springboot小区物业管理小程序---附源码45555
java·javascript·spring boot·python·django·flask·php
coder_lorraine1 小时前
Halo 2.x 回收站文章无法删除?一篇文章教你彻底解决
java·运维