适配器模式实战:接口兼容与Java改造案例解析

发布时间:2026/10/11 2:12:39
适配器模式实战:接口兼容与Java改造案例解析 1. 适配器模式到底在解决什么问题适配器模式是结构型设计模式里最容易被低估的一个。表面上看它就是加一层中转类把两个不匹配的接口接起来实际上它是很多大型系统能活着演进下去的关键。你会发现当业务代码被迫依赖一个外部老接口方法名、参数类型、返回结果全不是你要的样子又改不了对方源码的时候适配器模式就是那个把脏乱差统一收口的工具。它本质解决的是接口兼容性问题。无论是接第三方短信服务、对接老系统、整合不同数据库访问还是适配各种 SDK只要双方接口不匹配都可以用这个模式做隔离和转换。很多新手容易把适配器模式理解成“加一个中间类”这么一句废话但真正的问题在于加在哪儿、怎么加、加到什么程度才不会把系统改崩。这篇文章我想把适配器模式从概念到落地讲透包括四种常见写法、选型依据、一个完整的 Java 改造案例以及我实际排查过程中踩过的坑。1.1 先从一个真实到不想再经历的场景说起假设你负责的消息中心正在推进接口统一内部定义了一个很干净的发送接口MessageSender.send(Message)。但上个季度采购的短信供应商只提供了一个老旧的 SDK里面暴露的方法是int pushSms(String mobile, String content)。返回值 0 表示成功其他数字表示各种业务错误码而且 mobile 参数只认 11 位数字传带区号的字符串直接拒绝。如果你在业务代码里直接调用pushSms问题会立刻扩散所有调用消息中心的模块都要知道这个老 SDK 的方法签名以后换供应商改的就不是一个地方而是几十个调用点。适配器模式要做的事就是把pushSms包装成一个符合内部MessageSender.send约定的实现类让上层业务代码从头到尾只知道MessageSender不关心背后到底是短信、邮件还是 App 推送。这个场景我几乎每天都能在实际项目里看到。凡是系统活得够久内部标准接口和外部老旧接口之间必然有一堆“翻译层”翻译层就是适配器。1.2 为什么它会被归到结构型设计模式设计模式分三类创建型、结构型、行为型。创建型关注对象怎么产生行为型关注对象之间的职责和算法怎么分配结构型则关注类和对象怎么组合成更大的结构。适配器看中的不是“怎么创建适配器类”也不是“发送消息时谁先调用谁”而是“如何用组合或继承的方式让两个不兼容的接口放到同一个结构里还不起冲突”。从 GoF 的分类来看适配器模式又被分成“类适配器”和“对象适配器”两类。类适配器靠继承 Adaptee 实现 Target 接口对象适配器靠组合持有 Adaptee 实例。你只需要记住类适配器写起来短但灵活性差对象适配器写起来稍多但大多数时候更安全。后面我会用代码把这两种写法都展示出来。2. 适配器模式的四种实现方式与选型依据适配器模式并不是只有一种写法。除了 GoF 里的类适配器和对象适配器实际工程里还经常出现接口适配器和双向适配器。理解这四种写法的区别是你能不能把模式用对的关键。2.1 核心角色与调用流程无论哪种写法适配器模式都离不开四个角色Target目标接口也就是客户端希望依赖的那个抽象。比如内部消息中心定义的MessageSender。Adaptee被适配者通常是一个已经存在的类或第三方 SDK它的接口不符合当前需求。Adapter适配器把 Adaptee 的接口转换成 Target 接口。Client客户端只依赖 Target完全不感知 Adaptee 和 Adapter。调用流程可以这样理解客户端调用Adapter.send(...)Adapter 内部把参数翻译成老 SDK 需要的格式再调用LegacySmsClient.pushSms(...)拿到老 SDK 的返回结果后Adapter 再做一次翻译转成客户端认识的SendResult。整个过程中客户端甚至不知道存在一个LegacySmsClient。生活里最典型的类比就是电源转换插头。你从国内带一个两脚插头的笔记本去国外墙上插座是三孔的、孔径还不一样这时候你不会去改笔记本也不会去拆墙而是买一个转换头。适配器就是这个转换头一边适配笔记本另一边适配墙上的插座。2.2 对象适配器90% 的场景都该选它对象适配器用组合实现Adapter 持有 Adaptee 的实例并且在实现 Target 接口方法时调用 Adaptee 的方法。组合的好处有三个第一Adaptee 的失败不会通过继承关系污染 Adapter 的语义第二Adapter 可以在运行时决定换哪一个 Adaptee比如测试环境用 Mock 实现生产环境用真实实现第三如果 Adaptee 是一个 final 类或者它的构造函数很复杂、依赖很多外部资源继承根本搞不定组合仍然可以工作。我个人的默认选择就是对象适配器。除非我正在写的代码足够简单、单一并且我很确定不会出现第二种被适配者否则不要为了少写几个字段就去用继承。2.3 类适配器能用但别急着用类适配器通过继承 Adaptee 来实现 Target 接口。Java 里是这样的public class SmsSenderClassAdapter extends LegacySmsClient implements MessageSender { Override public SendResult send(Message message) { int code pushSms(message.getTarget(), message.getContent()); return code 0 ? SendResult.ok() : SendResult.fail(String.valueOf(code), fail); } }看着很省事但有一个结构性限制Java 不支持多继承所以当你需要适配两个不同 Adaptee 的时候类适配器立刻失效。另一个问题是继承会把 Adaptee 的所有公有方法都暴露给 Adapter 的调用者比如上面的queryBalance()也会被继承客户端能绕过 Target 接口直接调它这实际上破坏了接口隔离。当然类适配器也不是完全没用。如果 Adaptee 是个很简单的类方法数量少并且你确认不会扩展那用类适配器可以少写很多委托代码。只是这种场景在我的职业生涯里真的不多。2.4 接口适配器省掉一堆空方法的变体接口适配器也叫缺省适配器它的核心是当一个目标接口包含大量方法而客户端只需要其中少数几个方法时用一个抽象类实现接口并给出所有方法的默认空实现子类只需要覆盖自己关心的方法。一个常见例子是 Java AWT 的MouseAdapter。MouseListener接口有 5 个方法但大多数场景只需要mousePressed或mouseClicked所以提供一个抽象类把另外 4 个方法空实现掉你的类继承MouseAdapter后只需要写一个方法。接口适配器表面上是适配器模式的变体但它适配的不是“两个方法签名不同的类”而是“一个接口的方法数量和客户端实际需要的方法数量之间的差异”。从模式分类上看它更接近“缺省适配”但很多团队学习材料里都把它归到适配器模式里讲所以这里一并说清楚。2.5 双向适配器两边互为 Target 的骚操作双向适配器是最容易被忽略的冷门写法。它同时实现两个接口让 A 接口的调用者可以通过 Adapter 调用 B 接口B 接口的调用者也可以反过来调用 A 接口。听起来很聪明但我建议谨慎使用。双向适配器把两个方向的转换逻辑都塞在同一个类里一旦两个接口的语义不同步这个类会迅速膨胀而且测试时很难把两个方向完全覆盖到。如果团队里有人不熟悉这种模式维护起来会非常痛苦。我一般只会在两个接口都受自己控制、且转换逻辑完全对称时才考虑它。3. 实操用适配器模式改造一个老系统对接案例理论讲得再多不如一个能跑起来的例子。下面我用 Java 17 写一个相对完整的消息中心对接案例把对象适配器从定义到接入完整走一遍。3.1 场景交代与需求约束当前系统里有一个内部消息中心新代码统一依赖MessageSender接口。老的短信供应商 SDK 是LegacySmsClient为外部采购代码我们拿不到源码也不能修改它。老 SDK 的调用方式是pushSms(String mobile, String content)返回int0 表示成功其他值表示业务失败。改造目标很明确在不修改老 SDK、不修改调用方业务代码的前提下让LegacySmsClient以一个MessageSender的形式接入消息中心。3.2 第一步定义目标接口与消息模型先定义客户端需要的样子。这里我刻意让Message字段比老 SDK 多这样才能体现适配器的“翻译”价值。public enum ChannelType { SMS, EMAIL, APP_PUSH } public class Message { private final ChannelType channel; private final String target; private final String title; private final String content; public Message(ChannelType channel, String target, String title, String content) { this.channel channel; this.target target; this.title title; this.content content; } public ChannelType getChannel() { return channel; } public String getTarget() { return target; } public String getTitle() { return title; } public String getContent() { return content; } } public class SendResult { private final boolean success; private final String code; private final String description; private SendResult(boolean success, String code, String description) { this.success success; this.code code; this.description description; } public static SendResult ok() { return new SendResult(true, 0, success); } public static SendResult fail(String code, String description) { return new SendResult(false, code, description); } public boolean isSuccess() { return success; } public String getCode() { return code; } public String getDescription() { return description; } } public interface MessageSender { SendResult send(Message message); }这里有一个很关键的认知MessageSender是目标接口它的方法签名完全服务于客户端的使用习惯。即使大多数发送操作只需要 target 和 content我也把 title 和 channel 放进来因为适配器模式的价值就是允许目标接口和实际实现之间存在差异。3.3 第二步分析老系统实现接下来是那个让人头疼的LegacySmsClient。为了模拟真实情况我把方法签名和返回语义设计得和老系统典型风格一致方法名是动词短语参数是基本类型返回结果是魔法数字。public class LegacySmsClient { public int pushSms(String mobile, String content) { if (mobile null || !mobile.matches(^1\\d{10}$)) { return 40001; } if (content null || content.isEmpty()) { return 40002; } // 这里调用供应商接口省略具体网络逻辑 return 0; } public int queryBalance() { // 查询短信余量 return 53520; } }你看这个类有明显的“历史遗留特征”pushSms返回 intqueryBalance也不是目标接口关心的。业务代码如果直接依赖它后续每次换供应商都要大改。我们要做的就是让LegacySmsClient变成一个可替换的细节。3.4 第三步写一个对象适配器接入对象适配器是最稳妥的选择。我构造一个SmsSenderAdapter持有LegacySmsClient实现MessageSender.send。public class SmsSenderAdapter implements MessageSender { private final LegacySmsClient legacyClient; public SmsSenderAdapter(LegacySmsClient legacyClient) { this.legacyClient legacyClient; } Override public SendResult send(Message message) { if (message null || message.getChannel() ! ChannelType.SMS) { return SendResult.fail(501, unsupported channel); } String mobile normalizeMobile(message.getTarget()); int code legacyClient.pushSms(mobile, message.getContent()); if (code 0) { return SendResult.ok(); } return SendResult.fail(String.valueOf(code), legacy service error, code code); } private String normalizeMobile(String target) { if (target null) { return null; } // 去掉区号、空格、横线之后剩 11 位数字 return target.replaceAll([^0-9], ); } }注意看send方法内部做了三件事第一校验 channel避免把邮件消息错误地交给短信客户端第二把 target 翻译成老 SDK 期望的 11 位手机号第三把老 SDK 返回的 int 错误码翻译成统一的SendResult。这三件事才是适配器的灵魂而不是简单的“转发一下方法调用”。3.5 第四步类适配器写法对比为了对比我再给出类适配器版本。如果LegacySmsClient不是 final 类直接继承是可行的。public class SmsSenderClassAdapter extends LegacySmsClient implements MessageSender { Override public SendResult send(Message message) { if (message null || message.getChannel() ! ChannelType.SMS) { return SendResult.fail(501, unsupported channel); } int code pushSms(message.getTarget(), message.getContent()); if (code 0) { return SendResult.ok(); } return SendResult.fail(String.valueOf(code), legacy service error, code code); } }代码短了不少但问题也很明显这个适配器继承了一个queryBalance()方法客户端如果拿到的是SmsSenderClassAdapter实例而不是MessageSender接口就能直接调用它。更关键的是如果老 SDK 后来被换成一个 final 类或者它的构造方法依赖 Spring Bean继承这条路就断了。3.6 改造后的效果验证改造完成后新业务模块这样使用MessageSender sender new SmsSenderAdapter(new LegacySmsClient()); SendResult result sender.send(new Message( ChannelType.SMS, 138 0013 8000, 验证码, 您的验证码是 123456 ));客户端只认识MessageSender它不知道背后调用的是老 SDK也不知道手机号里还有空格。等明年供应商换成一个新 SDK我们只需要再写一个NewSmsSenderAdapter implements MessageSender然后改一行注入代码。这就是适配器模式最直接的价值隔离变化收敛差异。4. JDK、Spring 里的适配器模式现成例子照抄适配器模式不是什么高深的武林秘籍很多你在用的现成 API 底层就是这个思想。把这些例子定位清楚比死记定义有用得多。4.1 JDK 里的三个经典适配器先看InputStreamReader。它把InputStream字节流适配成Reader字符流你从文件读字节然后用Reader的 API 一行一行读文本这期间InputStreamReader内部做了字节到字符的转换。这是一个典型的对象适配器。然后是Arrays.asList。它把数组适配成List这样数组就能用集合的 API。注意它返回的 List 在增删时会抛异常这个“限制”其实来源于数组长度固定的本质适配器没有把这个问题吞掉而是直接暴露出来这是正确做法。还有Collections.enumeration(Collection)它把一个现代 Collection 适配成老的Enumeration接口用来兼容那些还只接受Enumeration的遗留代码。这三个例子都很有代表性JDK 官方其实就是适配器模式的骨灰级用户。4.2 Spring MVC 的 HandlerAdapter 到底适配了谁Spring MVC 里的HandlerAdapter是适配器模式在框架层最典型的应用。DispatcherServlet从容器里拿到一个 handler 对象之后并不直接调用它而是先找到能处理这个 handler 的HandlerAdapter再由适配器统一执行调用。为什么要多绕这一层因为 handler 可能是Controller方法、原始Controller接口、HttpRequestHandler甚至是Servlet。它们的调用方式完全不同方法参数怎么解析、返回值怎么处理、拦截器怎么执行各有各的规则。HandlerAdapter把不同 handler 类型的调用差异隔离掉让DispatcherServlet始终只用一套流程。所以说适配器模式不是为了炫技它是在框架层解决“调用方希望统一被调用方天然多样”的手段。4.3 适配器模式和装饰器、门面、代理差在哪这几个模式在结构上都很像都是“包一层”但出发点完全不同。模式核心目的接口形态典型场景适配器让接口不兼容的类可以协作转换为目标接口老 SDK 接入统一接口装饰器给原对象增加职责接口保持不变加日志、加缓存、加重试门面给复杂子系统提供简化入口提供新的高层接口下单、支付、库存的组合编排代理控制原对象访问通常保持同一接口接口保持不变远程代理、安全代理一个最简单的记忆方式适配器是“改接口”装饰器是“加功能”门面是“做简化”代理是“做控制”。如果只是想把旧接口包成新接口那就是适配器如果还想在调用前后加日志那是装饰器或者代理不冲突但别混着用。5. 常见问题与排查技巧实录写适配器本身不难难的是写完不出问题出问题能定位。这一部分把我在实际项目中遇到的高频问题和你分享。5.1 适配器变成“大杂烩”一个适配器接十个系统最常见的坏味道是一个MessageSenderAdapter被不断加分支今天适配短信明天适配推送后天适配邮件类里堆满if (type ...)和switch。这样的“适配器”不再是适配器而是服务定位器加策略模式的缝合怪。正确的做法是每种被适配者单独建一个适配器类。如果客户端只想依赖一个MessageSender再引入工厂或 Spring 注入根据 channel 返回不同的适配器实例。适配器类应该保持小而专注一个适配器只做一件事并且只对自己对应的 Adaptee 负责。5.2 适配器把异常和错误信息吞没了老 SDK 用 int 返回错误码时最容易丢的是“上下文”。如果pushSms返回 40001可能是手机号格式错误也可能是账户被停用。适配器如果只是把 40001 转成统一 code忽略 description排查问题的时候就只能对着一个数字发愣。我的建议是适配器里做转换时尽量把原始错误信息拼接进去。比如返回SendResult.fail(String.valueOf(code), legacy service error, code code)这样日志里至少能看到老系统到底出了什么错。如果老系统有异常对象也不要直接吞掉先记录完整堆栈再转成目标接口的失败结果。5.3 被适配者不是完全匹配目标语义别硬适配这是比语法匹配更深一层的问题。有时目标接口语义是“发送消息”而老接口语义是“查询余额”你硬把它们凑在一起适配器就变成了一场灾难。比如老 SDK 里根本没有 push 方法只有一个fetchSmsStatus你需要的是发送它还提供了别的能力这时候不问业务规则直接包一层是错的。遇到这种情况先判断“我们要的语义”和“老系统实际能力”是否真的对齐。不对齐时该补的补、该绕的绕或者干脆不用适配器改用门面模式建一个服务来组合能力。适配器不是万能胶不能为了用模式而强行制造不存在的对应关系。5.4 排查适配器问题的三条实战路径第一先查“参数翻译”是不是出了问题。很多人排查半天最后发现是normalizeMobile把 target 里的手机号过滤坏了。给适配器里每一处参数转换写好日志比在客户端瞎猜有用得多。第二用“金丝雀对比法”。保留一条老逻辑路径让它和新适配器同时跑对一批历史数据做对比看输出结果是否一致。找差异时主要盯返回值映射表老的 0、1、2 分别对应新结果的哪个 code最容易写错。第三看测试覆盖。适配器是高频率修改的代码如果没给每个分支写单元测试后面换供应商时你根本不敢动它。我在实际项目里至少会覆盖成功路径、老 SDK 返回错误码、入参为空、type 不支持、手机号包含格式符号这几个分支。测试有时候比实现本身更值钱。6. 踩坑之后的几条个人经验6.1 三条我一直在用的判断标准我每次决定要不要用适配器模式时都会问自己三个问题接口是不是真的可以不改对方调用方是不是真的需要统一接口适配器里做的事情是不是只是“翻译”而不是“业务规则”三个问题如果答案都是肯定的就用对象适配器。如果前两个答案是肯定的但第三个答案是否定的我会先停下来想想是不是门面模式更合适。另外一条经验很朴素优先组合、避免继承。即使类适配器看起来更短但当 Adaptee 的方法列表变化时继承造成的隐式 API 泄露会让调用方越来越不守规矩。对象适配器多写的几行委托代码换来的是清晰的边界。6.2 适配器之后再往前走一步如果项目里有多个适配器且每个都在做类似“结果状态转换”“参数清洗”的事情我会考虑把公共转换逻辑抽出来。比如定义一个LegacyResultMapper专门把老系统的错误码 map 成新系统的 SendResult又比如提炼一个MobileNormalizer让所有适配器共用手机号清洗逻辑。这样适配器本身仍然很薄维护成本不会随着接入系统数量线性上涨。适配器模式看起来是设计模式里最“土”的那个但它解决的是每天都会遇到的现实问题世界上的系统接口永远不可能整齐划一。学会恰当地使用适配器不只是多记一个模式更是学会在“别人不能改、自己不想乱”的现实约束里做出优雅的隔离层。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询