本文 5000 字深度原创含完整代码示例和生产级落地方案。创作不易如果对你有帮助请点赞 收藏 ⭐ 关注 三连支持你的认可是我持续输出的最大动力本文是「设计模式实战解读」系列第七篇。系列文章统一按照定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ的结构展开每篇聚焦一个模式讲透。一句话定义适配器模式Adapter将一个类的接口转换成调用方期望的另一个接口使原本接口不兼容的类可以一起工作。归属结构型模式。一、没有适配器时的痛点假设你在做一个统一消息通知系统需要对接多个第三方通知渠道publicclassNotificationService{publicvoidnotify(StringuserId,Stringcontent){// 钉钉 SDKDingTalkClientdingClientnewDingTalkClient();dingClient.sendMarkdown(userId,通知,content,false);// 企业微信 SDKWeComwecomnewWeCom(corpId,secret);wecom.message().send(newTextMessage(userId,content));// 飞书 SDKLarkSenderlarkLarkSender.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问题调用方和具体 SDK 强耦合——换一个 SDK 版本调用方要跟着改无法统一处理——想加个发送失败重试的通用逻辑要给三个 SDK 各写一遍难以扩展——新加一个渠道如 Slack要在 NotificationService 里再加一坨代码无法多态——三个 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 对象适配器推荐通过组合持有被适配对象// 目标接口统一的消息发送能力publicinterfaceMessageNotifier{NotifyResultsend(StringuserId,Stringcontent);Stringchannel();// 标识渠道名}// 适配器1适配钉钉 SDKComponentpublicclassDingTalkNotifierAdapterimplementsMessageNotifier{privatefinalDingTalkClientdingTalkClient;publicDingTalkNotifierAdapter(DingTalkClientdingTalkClient){this.dingTalkClientdingTalkClient;}OverridepublicStringchannel(){returndingtalk;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){try{// 将统一接口的参数翻译成钉钉SDK的参数格式dingTalkClient.sendMarkdown(userId,系统通知,content,false);returnNotifyResult.success();}catch(DingTalkExceptione){returnNotifyResult.fail(e.getErrCode(),e.getErrMsg());}}}// 适配器2适配企业微信 SDKComponentpublicclassWeComNotifierAdapterimplementsMessageNotifier{privatefinalWeComwecom;publicWeComNotifierAdapter(WeComwecom){this.wecomwecom;}OverridepublicStringchannel(){returnwecom;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){try{SendResultresultwecom.message().send(newTextMessage(userId,content));returnresult.isSuccess()?NotifyResult.success():NotifyResult.fail(result.getErrCode(),result.getErrMsg());}catch(Exceptione){returnNotifyResult.fail(WECOM_ERROR,e.getMessage());}}}// 适配器3适配飞书 SDKComponentpublicclassLarkNotifierAdapterimplementsMessageNotifier{privatefinalLarkSenderlarkSender;publicLarkNotifierAdapter(LarkSenderlarkSender){this.larkSenderlarkSender;}OverridepublicStringchannel(){returnlark;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){try{LarkMsgmsgLarkMsg.builder().openId(userId).text(content).build();ResponseVoidresplarkSender.pushText(msg);returnresp.isSuccess()?NotifyResult.success():NotifyResult.fail(String.valueOf(resp.getCode()),resp.getMsg());}catch(Exceptione){returnNotifyResult.fail(LARK_ERROR,e.getMessage());}}}3.2 统一调用ServicepublicclassNotificationService{// Spring 自动注入所有适配器构建渠道 MapprivatefinalMapString,MessageNotifiernotifierMap;publicNotificationService(ListMessageNotifiernotifiers){this.notifierMapnotifiers.stream().collect(Collectors.toMap(MessageNotifier::channel,Function.identity()));}publicNotifyResultnotify(Stringchannel,StringuserId,Stringcontent){MessageNotifiernotifiernotifierMap.get(channel);if(notifiernull){thrownewBizException(Unsupported channel: channel);}returnnotifier.send(userId,content);}// 群发所有渠道publicListNotifyResultnotifyAll(StringuserId,Stringcontent){returnnotifierMap.values().stream().map(n-n.send(userId,content)).collect(Collectors.toList());}}新增一个渠道Slack只需要加一个SlackNotifierAdapter实现MessageNotifier接口即可——Service 层零改动。3.3 类适配器了解即可通过继承被适配类同时实现目标接口// 类适配器继承 Adaptee 实现 TargetJava 只能单继承有局限publicclassDingTalkNotifierClassAdapterextendsDingTalkClientimplementsMessageNotifier{OverridepublicStringchannel(){returndingtalk;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){// 直接调用父类方法继承来的this.sendMarkdown(userId,通知,content,false);returnNotifyResult.success();}}实际项目中很少用类适配器——Java 单继承限制太大且对象适配器更灵活。四、真实应用场景4.1 框架级应用框架适配器被适配者 → 目标接口Spring MVCHandlerAdapter各种 Controller → 统一的处理器接口SLF4Jslf4j-log4j12 / logback-classicLog4j/Logback → SLF4J APISpring SecurityUserDetailsService任意用户存储 → Spring Security 用户体系JDBCDriver各数据库协议 → JDBC 标准接口JacksonJsonSerializer / 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统一 FileStoreSDK 不一致4.4 iPaaS 连接器的适配器体系在 iPaaS 平台中每个连接器就是一个适配器——将千差万别的第三方 API 适配为平台统一的执行接口// 平台统一的连接器执行接口TargetpublicinterfaceConnectorExecutor{ExecuteResultexecute(ConnectorRequestrequest);StringconnectorType();}// 钉钉连接器适配器ComponentpublicclassDingTalkConnectorAdapterimplementsConnectorExecutor{OverridepublicStringconnectorType(){returndingtalk;}OverridepublicExecuteResultexecute(ConnectorRequestrequest){// 1. 从统一请求中提取参数Stringactionrequest.getAction();// send_message / create_taskMapString,Objectparamsrequest.getParams();// 2. 翻译为钉钉 API 的格式DingTalkApiRequestdingReqDingTalkRequestBuilder.build(action,params);// 3. 调用钉钉 APIDingTalkApiResponsedingRespdingTalkHttpClient.call(dingReq);// 4. 将钉钉响应翻译回统一格式returnExecuteResult.builder().success(dingResp.getErrcode()0).data(dingResp.getBody()).errorMsg(dingResp.getErrmsg()).build();}}// 企业微信连接器适配器ComponentpublicclassWeComConnectorAdapterimplementsConnectorExecutor{OverridepublicStringconnectorType(){returnwecom;}OverridepublicExecuteResultexecute(ConnectorRequestrequest){// 相同的翻译逻辑但对接企微的 API 格式WeComRequestwecomReqWeComRequestBuilder.build(request.getAction(),request.getParams());WeComResponsewecomRespwecomClient.invoke(wecomReq);returnExecuteResult.from(wecomResp);}}iPaaS 平台对接 600 应用每个连接器本质上就是一个适配器——将各种不兼容的第三方 API 翻译为平台统一的 ConnectorRequest/ExecuteResult 协议。五、常见变种5.1 双向适配器同时实现两边的接口可以在两个系统之间双向转换// 既能把 NewLogger 当 OldLogger 用也能把 OldLogger 当 NewLogger 用publicclassLoggerBidirectionalAdapterimplementsOldLogger,NewLogger{privateOldLoggeroldLogger;privateNewLoggernewLogger;// 当作 OldLogger 使用时OverridepublicvoidwriteLog(Stringmsg){if(newLogger!null){newLogger.log(Level.INFO,msg,null);}}// 当作 NewLogger 使用时Overridepublicvoidlog(Levellevel,Stringmsg,Throwablet){if(oldLogger!null){oldLogger.writeLog([level] msg);}}}适用场景新老系统并行期间需要双向兼容。5.2 默认适配器缺省适配器当接口方法太多、但只想实现其中几个时用一个抽象类提供空实现// 接口有 10 个方法publicinterfaceEventListener{voidonStart(Evente);voidonPause(Evente);voidonResume(Evente);voidonStop(Evente);voidonError(Evente);// ... 更多方法}// 默认适配器所有方法空实现publicabstractclassEventListenerAdapterimplementsEventListener{OverridepublicvoidonStart(Evente){}OverridepublicvoidonPause(Evente){}OverridepublicvoidonResume(Evente){}OverridepublicvoidonStop(Evente){}OverridepublicvoidonError(Evente){}}// 使用时只覆写关心的方法publicclassMyListenerextendsEventListenerAdapter{OverridepublicvoidonError(Evente){alertService.send(发生异常: e.getMessage());}}Spring 的WebMvcConfigurerAdapter已废弃就是这个思路现在 Java 8 接口默认方法取代了它。5.3 适配器 工厂用工厂根据类型选择合适的适配器ComponentpublicclassNotifierAdapterFactory{privatefinalMapString,MessageNotifieradapterMap;publicNotifierAdapterFactory(ListMessageNotifieradapters){this.adapterMapadapters.stream().collect(Collectors.toMap(MessageNotifier::channel,Function.identity()));}publicMessageNotifiergetAdapter(Stringchannel){MessageNotifieradapteradapterMap.get(channel);if(adapternull){thrownewBizException(No adapter found for channel: channel);}returnadapter;}}六、优缺点优点缺点让不兼容的类协同工作增加了一层间接性适配层复用已有类不修改其代码适配器过多时系统结构变复杂符合单一职责适配逻辑集中在 Adapter类适配器受 Java 单继承限制符合开闭原则新增适配器不改已有代码过度使用适配器可能是接口设计有问题的信号解耦调用方和第三方 SDK适配器本身可能变成胖类转换逻辑复杂时七、避坑指南坑 1适配器内做业务逻辑// ❌ 适配器不应该包含业务逻辑publicclassDingTalkNotifierAdapterimplementsMessageNotifier{OverridepublicNotifyResultsend(StringuserId,Stringcontent){// 这些是业务逻辑不该放在适配器里if(isNightTime()){returnNotifyResult.skip(夜间免打扰);}contentsensitiveWordFilter(content);// 敏感词过滤dingTalkClient.sendMarkdown(...);}}// ✓ 适配器只做接口转换业务逻辑在 Service 层publicclassDingTalkNotifierAdapterimplementsMessageNotifier{OverridepublicNotifyResultsend(StringuserId,Stringcontent){// 只做翻译统一参数 → 钉钉参数dingTalkClient.sendMarkdown(userId,通知,content,false);returnNotifyResult.success();}}原则适配器只负责格式转换不做业务决策。坑 2异常没有统一转换// ❌ 适配器直接抛出 SDK 特有异常publicNotifyResultsend(StringuserId,Stringcontent){dingTalkClient.sendMarkdown(...);// 抛 DingTalkException// 调用方还要 catch DingTalkException耦合没有解除}// ✓ 统一转换为平台异常publicNotifyResultsend(StringuserId,Stringcontent){try{dingTalkClient.sendMarkdown(...);returnNotifyResult.success();}catch(DingTalkExceptione){// 翻译异常对外暴露统一的错误模型returnNotifyResult.fail(e.getErrCode(),e.getErrMsg());}}坑 3适配器数据丢失第三方 API 返回了 10 个字段但你的 Target 接口只定义了 3 个字段——其他 7 个信息就丢了。解法Target 接口设计时预留MapString, Object extra扩展字段或用泛型支持扩展。publicclassExecuteResult{privatebooleansuccess;privateObjectdata;privateStringerrorMsg;privateMapString,Objectextra;// 预留扩展放不进标准字段的信息}坑 4适配器与 Adaptee 版本耦合第三方 SDK 升级后方法签名变了适配器编译失败。解法适配器模块单独维护不要散落在业务代码中适配器内加版本判断或抽取SdkVersion策略用 Maven/Gradle 模块隔离适配器依赖八、常见问题FAQQ适配器模式和装饰器模式都是包一层有什么区别A核心意图不同适配器接口不兼容要把 A 接口转换成 B 接口——改变接口形态装饰器接口相同要给现有能力添加新功能——增强同一个接口适配器的 wrapped 对象和 Target 接口不同装饰器的 wrapped 对象和自身相同接口。Q适配器模式和外观模式Facade有什么区别A适配器一对一转换一个 Adaptee 适配为一个 Target外观多对一封装多个子系统组合为一个简洁接口外观模式侧重简化复杂子系统的使用适配器侧重兼容不同接口。Q什么时候该用适配器什么时候该重新设计接口A用适配器Adaptee 是你无法修改的第三方 SDK、旧系统、已发布的 API重新设计如果是自己团队的代码且接口设计本身有问题应该直接改接口而不是堆适配器掩盖设计缺陷QSpring MVC 的 HandlerAdapter 为什么叫适配器ASpring MVC 支持多种 Controller 写法Controller注解、实现Controller接口、实现HttpRequestHandler接口。DispatcherServlet需要一个统一的方式来调用它们——HandlerAdapter就是适配器把不同类型的 Controller 适配为统一的handle(request, response, handler)调用。Q一个适配器可以适配多个 Adaptee 吗A可以但不推荐。一个适配器适配多个 Adaptee 会导致适配器变成胖类违反单一职责。推荐一个 Adaptee 对应一个 Adapter。九、小结适配器模式的核心价值让已有的、接口不兼容的代码能无缝接入你的系统不修改任何一方的原始代码。三个实践要点适配器只做翻译不做业务逻辑——格式转换、异常转换、字段映射仅此而已异常必须统一转换——不能让 SDK 特有异常穿透适配层泄露给调用方优先用对象适配器组合而非类适配器继承——更灵活不受单继承限制下一篇我们聊代理模式——当你需要控制对一个对象的访问权限校验、懒加载、远程调用、限流而不想让调用方感知这层控制时代理是最佳选择。标签#设计模式 #适配器模式 #Adapter #结构型模式 #Java #Spring #接口兼容 #第三方SDK #连接器适配 #SLF4J #iPaaS #面向对象 #软件工程
设计模式实战解读(七):适配器模式——让不兼容的接口无缝协作
发布时间:2026/6/22 8:19:49
本文 5000 字深度原创含完整代码示例和生产级落地方案。创作不易如果对你有帮助请点赞 收藏 ⭐ 关注 三连支持你的认可是我持续输出的最大动力本文是「设计模式实战解读」系列第七篇。系列文章统一按照定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ的结构展开每篇聚焦一个模式讲透。一句话定义适配器模式Adapter将一个类的接口转换成调用方期望的另一个接口使原本接口不兼容的类可以一起工作。归属结构型模式。一、没有适配器时的痛点假设你在做一个统一消息通知系统需要对接多个第三方通知渠道publicclassNotificationService{publicvoidnotify(StringuserId,Stringcontent){// 钉钉 SDKDingTalkClientdingClientnewDingTalkClient();dingClient.sendMarkdown(userId,通知,content,false);// 企业微信 SDKWeComwecomnewWeCom(corpId,secret);wecom.message().send(newTextMessage(userId,content));// 飞书 SDKLarkSenderlarkLarkSender.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问题调用方和具体 SDK 强耦合——换一个 SDK 版本调用方要跟着改无法统一处理——想加个发送失败重试的通用逻辑要给三个 SDK 各写一遍难以扩展——新加一个渠道如 Slack要在 NotificationService 里再加一坨代码无法多态——三个 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 对象适配器推荐通过组合持有被适配对象// 目标接口统一的消息发送能力publicinterfaceMessageNotifier{NotifyResultsend(StringuserId,Stringcontent);Stringchannel();// 标识渠道名}// 适配器1适配钉钉 SDKComponentpublicclassDingTalkNotifierAdapterimplementsMessageNotifier{privatefinalDingTalkClientdingTalkClient;publicDingTalkNotifierAdapter(DingTalkClientdingTalkClient){this.dingTalkClientdingTalkClient;}OverridepublicStringchannel(){returndingtalk;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){try{// 将统一接口的参数翻译成钉钉SDK的参数格式dingTalkClient.sendMarkdown(userId,系统通知,content,false);returnNotifyResult.success();}catch(DingTalkExceptione){returnNotifyResult.fail(e.getErrCode(),e.getErrMsg());}}}// 适配器2适配企业微信 SDKComponentpublicclassWeComNotifierAdapterimplementsMessageNotifier{privatefinalWeComwecom;publicWeComNotifierAdapter(WeComwecom){this.wecomwecom;}OverridepublicStringchannel(){returnwecom;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){try{SendResultresultwecom.message().send(newTextMessage(userId,content));returnresult.isSuccess()?NotifyResult.success():NotifyResult.fail(result.getErrCode(),result.getErrMsg());}catch(Exceptione){returnNotifyResult.fail(WECOM_ERROR,e.getMessage());}}}// 适配器3适配飞书 SDKComponentpublicclassLarkNotifierAdapterimplementsMessageNotifier{privatefinalLarkSenderlarkSender;publicLarkNotifierAdapter(LarkSenderlarkSender){this.larkSenderlarkSender;}OverridepublicStringchannel(){returnlark;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){try{LarkMsgmsgLarkMsg.builder().openId(userId).text(content).build();ResponseVoidresplarkSender.pushText(msg);returnresp.isSuccess()?NotifyResult.success():NotifyResult.fail(String.valueOf(resp.getCode()),resp.getMsg());}catch(Exceptione){returnNotifyResult.fail(LARK_ERROR,e.getMessage());}}}3.2 统一调用ServicepublicclassNotificationService{// Spring 自动注入所有适配器构建渠道 MapprivatefinalMapString,MessageNotifiernotifierMap;publicNotificationService(ListMessageNotifiernotifiers){this.notifierMapnotifiers.stream().collect(Collectors.toMap(MessageNotifier::channel,Function.identity()));}publicNotifyResultnotify(Stringchannel,StringuserId,Stringcontent){MessageNotifiernotifiernotifierMap.get(channel);if(notifiernull){thrownewBizException(Unsupported channel: channel);}returnnotifier.send(userId,content);}// 群发所有渠道publicListNotifyResultnotifyAll(StringuserId,Stringcontent){returnnotifierMap.values().stream().map(n-n.send(userId,content)).collect(Collectors.toList());}}新增一个渠道Slack只需要加一个SlackNotifierAdapter实现MessageNotifier接口即可——Service 层零改动。3.3 类适配器了解即可通过继承被适配类同时实现目标接口// 类适配器继承 Adaptee 实现 TargetJava 只能单继承有局限publicclassDingTalkNotifierClassAdapterextendsDingTalkClientimplementsMessageNotifier{OverridepublicStringchannel(){returndingtalk;}OverridepublicNotifyResultsend(StringuserId,Stringcontent){// 直接调用父类方法继承来的this.sendMarkdown(userId,通知,content,false);returnNotifyResult.success();}}实际项目中很少用类适配器——Java 单继承限制太大且对象适配器更灵活。四、真实应用场景4.1 框架级应用框架适配器被适配者 → 目标接口Spring MVCHandlerAdapter各种 Controller → 统一的处理器接口SLF4Jslf4j-log4j12 / logback-classicLog4j/Logback → SLF4J APISpring SecurityUserDetailsService任意用户存储 → Spring Security 用户体系JDBCDriver各数据库协议 → JDBC 标准接口JacksonJsonSerializer / 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统一 FileStoreSDK 不一致4.4 iPaaS 连接器的适配器体系在 iPaaS 平台中每个连接器就是一个适配器——将千差万别的第三方 API 适配为平台统一的执行接口// 平台统一的连接器执行接口TargetpublicinterfaceConnectorExecutor{ExecuteResultexecute(ConnectorRequestrequest);StringconnectorType();}// 钉钉连接器适配器ComponentpublicclassDingTalkConnectorAdapterimplementsConnectorExecutor{OverridepublicStringconnectorType(){returndingtalk;}OverridepublicExecuteResultexecute(ConnectorRequestrequest){// 1. 从统一请求中提取参数Stringactionrequest.getAction();// send_message / create_taskMapString,Objectparamsrequest.getParams();// 2. 翻译为钉钉 API 的格式DingTalkApiRequestdingReqDingTalkRequestBuilder.build(action,params);// 3. 调用钉钉 APIDingTalkApiResponsedingRespdingTalkHttpClient.call(dingReq);// 4. 将钉钉响应翻译回统一格式returnExecuteResult.builder().success(dingResp.getErrcode()0).data(dingResp.getBody()).errorMsg(dingResp.getErrmsg()).build();}}// 企业微信连接器适配器ComponentpublicclassWeComConnectorAdapterimplementsConnectorExecutor{OverridepublicStringconnectorType(){returnwecom;}OverridepublicExecuteResultexecute(ConnectorRequestrequest){// 相同的翻译逻辑但对接企微的 API 格式WeComRequestwecomReqWeComRequestBuilder.build(request.getAction(),request.getParams());WeComResponsewecomRespwecomClient.invoke(wecomReq);returnExecuteResult.from(wecomResp);}}iPaaS 平台对接 600 应用每个连接器本质上就是一个适配器——将各种不兼容的第三方 API 翻译为平台统一的 ConnectorRequest/ExecuteResult 协议。五、常见变种5.1 双向适配器同时实现两边的接口可以在两个系统之间双向转换// 既能把 NewLogger 当 OldLogger 用也能把 OldLogger 当 NewLogger 用publicclassLoggerBidirectionalAdapterimplementsOldLogger,NewLogger{privateOldLoggeroldLogger;privateNewLoggernewLogger;// 当作 OldLogger 使用时OverridepublicvoidwriteLog(Stringmsg){if(newLogger!null){newLogger.log(Level.INFO,msg,null);}}// 当作 NewLogger 使用时Overridepublicvoidlog(Levellevel,Stringmsg,Throwablet){if(oldLogger!null){oldLogger.writeLog([level] msg);}}}适用场景新老系统并行期间需要双向兼容。5.2 默认适配器缺省适配器当接口方法太多、但只想实现其中几个时用一个抽象类提供空实现// 接口有 10 个方法publicinterfaceEventListener{voidonStart(Evente);voidonPause(Evente);voidonResume(Evente);voidonStop(Evente);voidonError(Evente);// ... 更多方法}// 默认适配器所有方法空实现publicabstractclassEventListenerAdapterimplementsEventListener{OverridepublicvoidonStart(Evente){}OverridepublicvoidonPause(Evente){}OverridepublicvoidonResume(Evente){}OverridepublicvoidonStop(Evente){}OverridepublicvoidonError(Evente){}}// 使用时只覆写关心的方法publicclassMyListenerextendsEventListenerAdapter{OverridepublicvoidonError(Evente){alertService.send(发生异常: e.getMessage());}}Spring 的WebMvcConfigurerAdapter已废弃就是这个思路现在 Java 8 接口默认方法取代了它。5.3 适配器 工厂用工厂根据类型选择合适的适配器ComponentpublicclassNotifierAdapterFactory{privatefinalMapString,MessageNotifieradapterMap;publicNotifierAdapterFactory(ListMessageNotifieradapters){this.adapterMapadapters.stream().collect(Collectors.toMap(MessageNotifier::channel,Function.identity()));}publicMessageNotifiergetAdapter(Stringchannel){MessageNotifieradapteradapterMap.get(channel);if(adapternull){thrownewBizException(No adapter found for channel: channel);}returnadapter;}}六、优缺点优点缺点让不兼容的类协同工作增加了一层间接性适配层复用已有类不修改其代码适配器过多时系统结构变复杂符合单一职责适配逻辑集中在 Adapter类适配器受 Java 单继承限制符合开闭原则新增适配器不改已有代码过度使用适配器可能是接口设计有问题的信号解耦调用方和第三方 SDK适配器本身可能变成胖类转换逻辑复杂时七、避坑指南坑 1适配器内做业务逻辑// ❌ 适配器不应该包含业务逻辑publicclassDingTalkNotifierAdapterimplementsMessageNotifier{OverridepublicNotifyResultsend(StringuserId,Stringcontent){// 这些是业务逻辑不该放在适配器里if(isNightTime()){returnNotifyResult.skip(夜间免打扰);}contentsensitiveWordFilter(content);// 敏感词过滤dingTalkClient.sendMarkdown(...);}}// ✓ 适配器只做接口转换业务逻辑在 Service 层publicclassDingTalkNotifierAdapterimplementsMessageNotifier{OverridepublicNotifyResultsend(StringuserId,Stringcontent){// 只做翻译统一参数 → 钉钉参数dingTalkClient.sendMarkdown(userId,通知,content,false);returnNotifyResult.success();}}原则适配器只负责格式转换不做业务决策。坑 2异常没有统一转换// ❌ 适配器直接抛出 SDK 特有异常publicNotifyResultsend(StringuserId,Stringcontent){dingTalkClient.sendMarkdown(...);// 抛 DingTalkException// 调用方还要 catch DingTalkException耦合没有解除}// ✓ 统一转换为平台异常publicNotifyResultsend(StringuserId,Stringcontent){try{dingTalkClient.sendMarkdown(...);returnNotifyResult.success();}catch(DingTalkExceptione){// 翻译异常对外暴露统一的错误模型returnNotifyResult.fail(e.getErrCode(),e.getErrMsg());}}坑 3适配器数据丢失第三方 API 返回了 10 个字段但你的 Target 接口只定义了 3 个字段——其他 7 个信息就丢了。解法Target 接口设计时预留MapString, Object extra扩展字段或用泛型支持扩展。publicclassExecuteResult{privatebooleansuccess;privateObjectdata;privateStringerrorMsg;privateMapString,Objectextra;// 预留扩展放不进标准字段的信息}坑 4适配器与 Adaptee 版本耦合第三方 SDK 升级后方法签名变了适配器编译失败。解法适配器模块单独维护不要散落在业务代码中适配器内加版本判断或抽取SdkVersion策略用 Maven/Gradle 模块隔离适配器依赖八、常见问题FAQQ适配器模式和装饰器模式都是包一层有什么区别A核心意图不同适配器接口不兼容要把 A 接口转换成 B 接口——改变接口形态装饰器接口相同要给现有能力添加新功能——增强同一个接口适配器的 wrapped 对象和 Target 接口不同装饰器的 wrapped 对象和自身相同接口。Q适配器模式和外观模式Facade有什么区别A适配器一对一转换一个 Adaptee 适配为一个 Target外观多对一封装多个子系统组合为一个简洁接口外观模式侧重简化复杂子系统的使用适配器侧重兼容不同接口。Q什么时候该用适配器什么时候该重新设计接口A用适配器Adaptee 是你无法修改的第三方 SDK、旧系统、已发布的 API重新设计如果是自己团队的代码且接口设计本身有问题应该直接改接口而不是堆适配器掩盖设计缺陷QSpring MVC 的 HandlerAdapter 为什么叫适配器ASpring MVC 支持多种 Controller 写法Controller注解、实现Controller接口、实现HttpRequestHandler接口。DispatcherServlet需要一个统一的方式来调用它们——HandlerAdapter就是适配器把不同类型的 Controller 适配为统一的handle(request, response, handler)调用。Q一个适配器可以适配多个 Adaptee 吗A可以但不推荐。一个适配器适配多个 Adaptee 会导致适配器变成胖类违反单一职责。推荐一个 Adaptee 对应一个 Adapter。九、小结适配器模式的核心价值让已有的、接口不兼容的代码能无缝接入你的系统不修改任何一方的原始代码。三个实践要点适配器只做翻译不做业务逻辑——格式转换、异常转换、字段映射仅此而已异常必须统一转换——不能让 SDK 特有异常穿透适配层泄露给调用方优先用对象适配器组合而非类适配器继承——更灵活不受单继承限制下一篇我们聊代理模式——当你需要控制对一个对象的访问权限校验、懒加载、远程调用、限流而不想让调用方感知这层控制时代理是最佳选择。标签#设计模式 #适配器模式 #Adapter #结构型模式 #Java #Spring #接口兼容 #第三方SDK #连接器适配 #SLF4J #iPaaS #面向对象 #软件工程