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

🔔 本文 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 #面向对象 #软件工程

相关推荐
garmin Chen2 小时前
rabbitmq(1):核心机制与 SpringAMQP 详解
java·rabbitmq·java-rabbitmq
Mr_sst2 小时前
AI 大模型应用开发实习|如何找岗 + 面试真题 + 面经总结
java·人工智能·ai·面试·职场和发展
weelinking2 小时前
【产品】10_搭建前端框架——把你的原型变成真实页面
java·大数据·前端·数据库·人工智能·python·前端框架
一 乐2 小时前
图书电子商务网站系统|基于SprinBoot+vue图书电子商务网站设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·图书电子商务网站系统
西敏寺的乐章2 小时前
01-倒排索引原理-搜索引擎为什么能秒搜
java·elasticsearch·搜索引擎
yaoxin5211232 小时前
421. Java 日期时间 API - 包结构 & 方法命名规范
java·前端·python
方也_arkling10 小时前
【Java-Day08】static / final / 枚举
java·开发语言
橙淮11 小时前
Spring Bean作用域与生命周期全解析
java·spring
Chengbei1111 小时前
一站式源码安全检测工具、云安全 / APP / 小程序源码敏感信息递归多层目录扫描AK、JWT、手机号、身份证等敏感信息
java·开发语言·安全·web安全·网络安全·系统安全·安全架构