资讯动态

适配器模式实战:类适配器与对象适配器如何选型

发布时间:2026/10/16 12:21:16 来源:尧图企业网站定制
做后端开发的这些年我几乎每年都会遇到同一个尴尬手里维护的还是那套吭哧吭哧跑了好几年的老类可新需求、新SDK、新网关全都只认另一套接口格式。改老代码怕影响线上推倒重写工期又不够这时候最实用的解法就是加一个“翻译层”让老接口在新接口面前看起来“天生无缝”。这个套路在设计模式里有个正式名字——适配器模式它专门解决已有类接口不兼容的问题并且按实现方式分成两个流派类适配器用继承实现和对象适配器用组合实现。这篇文章不打算把适配器模式捧成银弹只想借一个真实的支付模块改造场景把类适配器和对象适配器的代码差异、取舍逻辑和实战选型掰开讲清楚。适合谁看如果你是刚接触设计模式的初学者这里面的代码例子可以直接抄如果你已经在项目里写过Adapter后面选型对比和老系统迁移那两节应该能帮你少踩几个坑。1. 老接口和新需求之间缺的不是重写而是一个翻译层1.1 现实中每天都在发生的“接口不兼容”先想清楚一个问题什么叫“已有类接口不兼容”它不是报错提示里那种语法层面的不兼容而是一种更隐蔽的摩擦。比如你项目里有一个运行了多年的支付服务类它的方法叫payByXml(String xml)输入输出都是XML字符串。这个类被十几个老模块调用稳定性经过验证没人敢大动。现在公司要接一套新的统一结算平台对外只暴露一个标准接口方法长这样PaymentReceipt pay(PaymentRequest request)参数是一个对象返回也是一个对象。两边都能正常工作但新系统根本不知道老类存在老类也听不懂新接口的对象语言。这种局面的本质是什么是双方都只认自己的“方言”而业务能力其实完全对得上。就像你从国内带了一个两脚插头到了另一个国家发现插座是三脚的你要的不是换掉整个电器也不是拆了墙重装插座而是买一个转换头。适配器模式干的就是这件事在不修改老类内部代码的前提下在调用方和老类之间插入一层适配代码把老类的方言“翻译”成新接口能听懂的普通话。调用方不需要知道底层是谁在干活老类也不需要知道自己正在被新系统使用。1.2 适配器模式三个参与者的职责这套模式里一共有三个角色搞清楚它们的职责后面看代码会轻松很多。目标接口Target新系统、新调用方期望的接口标准也就是上文的NewPaymentApi。被适配者Adaptee那个已经存在、内部逻辑良好但接口不合规的老类也就是LegacyPaymentService。适配器Adapter我们新写的那个“翻译层”负责把目标接口的调用转换成被适配者的调用再转换返回结果。这里有个很多人容易忽略的点适配器模式并不要求修改被适配者的源码它只是站在中间做转换。所以它天然适合那些“代码不是你的、但问题却是你的”的场景——比如依赖的第三方库版本升级接口变了、公司内部A系统要对接B系统的老协议、或者你接手了一个别人留下的上古代码。提示适配器解决的是“接口形状不一样”不是“业务逻辑不一样”。如果老类根本没有你要的能力那不叫适配那叫缺功能别硬用适配器兜底。2. 类适配器用继承把旧实现“伪装”成新接口2.1 类适配器的代码长什么样类适配器也就是有些资料里写作“分类适配器”的那种实现方式核心思路非常直接让适配器继承老类同时实现新接口。这样适配器既是老类的子类又具备新接口的形状一举两得。拿上面支付场景来写代码是这样的// 目标接口新统一支付平台定义的标准接口 public interface NewPaymentApi { PaymentReceipt pay(PaymentRequest request); } // 被适配者已经在线上稳定运行多年的老支付服务 public class LegacyPaymentService { public String payByXml(String xmlRequest) { // 老逻辑参数和返回值都是XML字符串 return resultorderId123/orderIdstatusSUCCESS/status/result; } } // 类适配器继承老实现实现新接口 public class LegacyPaymentClassAdapter extends LegacyPaymentService implements NewPaymentApi { Override public PaymentReceipt pay(PaymentRequest request) { // 1. 新接口的请求对象转换成老接口要的XML String xmlRequest convertToXml(request); // 2. 调用从父类继承来的老方法 String xmlResult payByXml(xmlRequest); // 3. 老接口的XML结果解析成新接口的标准返回对象 return parseResult(xmlResult); } private String convertToXml(PaymentRequest request) { // 这里做对象到XML的序列化 return requestorderId request.getOrderId() /orderId/request; } private PaymentReceipt parseResult(String xmlResult) { // 这里做XML到对象的反序列化 return new PaymentReceipt(xmlResult); } }调用方就很舒服了它面对的是统一的NewPaymentApi完全不感知底层是老类还是新实现NewPaymentApi api new LegacyPaymentClassAdapter(); PaymentReceipt receipt api.pay(request);这个写法的好处是代码量最小。不需要单独持有老类的实例老类的方法直接通过继承链变成适配器的能力转换逻辑集中在一个类里。如果你需要适配的老类方法不多继承方式写起来确实爽。2.2 为什么继承这条路走起来会越来越窄但是在真实项目里我见过太多类适配器写完之后被后人骂街的情况。问题不是这个模式错了而是继承这个手段本身有硬伤。第一个限制是单继承。Java、C#这类主流静态语言一个类只能继承一个父类。一旦适配器继承了老类它就没有位置再继承别的基类了。如果被适配者本身不是接口而是抽象类或者老类后面又增长了新的方法适配器会越写越臃肿最终变成一个既当适配器、又当业务实现类的“缝合怪”。第二个限制是方法暴露过多。继承意味着适配器把老类的所有public方法都继承过来了哪怕这些方法根本不属于新接口。调用方虽然声明的是NewPaymentApi类型但适配器的实际类型里混入了大量和老接口相关的杂音。团队里如果有新人拿到这个适配器对象拿去直接调用很容易绕过新接口标准直接把老方法当成新能力来用接口边界就此被突破。第三个限制是可适配范围窄。继承是编译期绑定死的它只能适配那一个具体的老类。如果后续出现一个新的老类变体比如LegacyPaymentServiceV2类适配器没法动态切换得再写一个新的适配器子类。而组合方式可以通过构造参数灵活换对象。这里我给一个判断依据只有当你明确需要覆写老类的某些方法行为或者需要访问老类中受保护的成员时才优先考虑类适配器。否则组合往往更适合。3. 对象适配器用组合把旧对象“装进”新外壳3.1 对象适配器的代码长什么样对象适配器的思路换了一个方向适配器不继承老类而是持有一个老类实例通过调用实例方法完成转换。所谓“组合优先于继承”在这块体现得很典型。// 目标接口不变 public interface NewPaymentApi { PaymentReceipt pay(PaymentRequest request); } // 被适配者不变 public class LegacyPaymentService { public String payByXml(String xmlRequest) { return resultorderId123/orderIdstatusSUCCESS/status/result; } } // 对象适配器组合老实例实现新接口 public class LegacyPaymentObjectAdapter implements NewPaymentApi { // 关键区别持有被适配者的引用而不是继承 private final LegacyPaymentService legacyService; public LegacyPaymentObjectAdapter(LegacyPaymentService legacyService) { this.legacyService legacyService; } Override public PaymentReceipt pay(PaymentRequest request) { String xmlRequest convertToXml(request); String xmlResult legacyService.payByXml(xmlRequest); return parseResult(xmlResult); } private String convertToXml(PaymentRequest request) { return requestorderId request.getOrderId() /orderId/request; } private PaymentReceipt parseResult(String xmlResult) { return new PaymentReceipt(xmlResult); } }调用方式也类似只是构造时多传一个老实例LegacyPaymentService legacyService new LegacyPaymentService(); NewPaymentApi api new LegacyPaymentObjectAdapter(legacyService); PaymentReceipt receipt api.pay(request);从代码量上看对象适配器只比类适配器多了一个字段和构造方法几乎不增加成本。但它换来的灵活性是实打实的。3.2 组合方案为什么往往是首选对象适配器最大的价值是你拿到的NewPaymentApi背后可以不只一个老对象。今天传入的是LegacyPaymentService明天想换成LegacyPaymentServiceV2只要它方法签名一致适配器代码一行都不用改。老对象在适配器之外按它自己的生命周期管理适配器随时可以换人。组合方案也不会污染接口面。适配器实现了哪些方法调用方就只能看到哪些方法老类上那些多余的public方法完全被隔离在适配器内部。接口边界清晰了后续做单元测试也更舒服。再考虑扩展性。组合方案天然适合“一组老接口要统一适配”的场景。比如你有两个老类一个负责对接银行卡支付一个负责对接余额支付两者方法不一样但新的统一接口要求暴露同一个pay方法。这时候写一个对象适配器内部聚合两个老对象就能在适配层完成逻辑装配。如果用继承这种多对象协作几乎写不出来。注意对象适配器在转换过程中依赖老对象的状态。如果老类内部持有线程不安全的成员变量而新接口需要支持高并发你要么先修老类要么在适配器层加同步控制不能指望适配器替你解决并发问题。3.3 顺手聊聊适配器模式的常见变体理解了这层结构后适配器模式其实还有几个变体实际项目里经常见到容易被初学者忽略。第一个是双向适配器。适配器同时实现两套接口既能从新接口调老类也能从老接口调新类。这种写法在老系统和新系统存在互相访问需求时很有用但要注意双向转换逻辑容易膨胀最好在类注释里写清楚哪边的转换是主路径。第二个是默认适配器缺省适配器。当新接口方法特别多而你只想实现其中一个时可以先写一个空实现的抽象类让真正的适配器继承它并覆写需要的方法。这是适配器在简化方向上的延伸尤其在监听器、回调接口场景里非常常见只是很多人不知道自己其实在用适配器模式。无论是哪个变体核心还是那句话转换逻辑放在适配器里业务逻辑留在老类里别在适配器里夹带私货。4. 实战选型先问三个问题再决定用继承还是组合4.1 选型之前先问自己三个问题很多读者看完前两节会问那到底什么时候用类适配器什么时候用对象适配器我的答案是不要背结论动手前先回答三个问题。第一被适配者的方法是你自己的还是别人jar包里的如果是第三方库的类而且你没有源码头类适配器的覆写能力基本用不上老老实实组合。第二老类有没有你不需要的public方法如果有而且你又不想开放给调用方那类适配器会“泄底”组合才是安全的防火墙。第三以后可能要适配的不止一个版本组合和接口配合随时换对象继承是编译期定死的后期如果出现新的老类版本你要么改代码要么复制粘贴新的适配器子类。这种扩展性差异在长期维护的项目里是致命的。为了让你直观对比我整理了一张选型对照表可以直接存下来当备忘。对比维度类适配器继承对象适配器组合实现方式继承老类 实现新接口持有老对象 实现新接口代码量略少多一个字段和构造方法暴露的方法面继承链上所有public方法都会暴露只暴露新接口定义的方法能否适配多个变体编译期固定不能动态切换构造时传对象可灵活替换能否访问protected成员可以不行能否适配多个老类到同一接口麻烦不推荐可以聚合多个对象语言限制Java/C#单继承会限制扩展无额外限制推荐度仅在需要覆写行为时使用默认优先选择4.2 适配器、门面、代理、装饰器别再傻傻分不清写适配器的时候还有一个高频困惑是这和门面模式、代理模式、装饰器模式看起来好像都差不多都是“包一层”。它们在代码形式上确实都是包装但意图完全不同。门面模式要解决的是“多对多”的简化。调用方面对一个复杂的子系统门面把一堆互相协作的老类整合成一个简单入口。适配器不整合多个类的协作它只翻译单个已有类的接口形状。代理模式完全不改变接口也不转换数据格式。代理和控制访问有关比如延迟加载、权限校验、远程调用代理调用方看到的接口和真实对象是一模一样的。适配器改的是接口本身代理改的是访问时机。装饰器模式也不改变接口它是在原有功能之上叠加能力。比如一个支付接口先用装饰器加一个风险校验再加一个日志记录。如果适配器在转换格式的同时又顺手加了日志或者权限判断那就混入了装饰器/代理的职责会在代码评审时被问住。鉴别方法其实就一句话接口变了是适配器接口不变是代理或装饰器多个对象合并成外观是门面。4.3 老系统里接入适配层的渐进式迁移经验最后分享一段真实项目里的改造过程这可能是最有参考价值的部分。当时我接手一个老订单系统老模块直接调用老支付服务新上线了一个财务核销平台只认标准JSON接口。一开始团队里最激进的人主张干脆重写老支付服务让大家统一走新接口。方案评审时被老系统负责人当场否了老服务里有大量历史兼容逻辑重写三个月的周期期间线上支付出任何问题都担不起这个责任。最终落地的是一个渐进式迁移方案先写对象适配器只做格式转换不动老逻辑。新财务平台接入适配器调用老服务老模块维持原样。这一步上线之后先在灰度环境跑了两个星期确认转换层的数据一致性和线上完全对齐。然后第二步老模块逐个切换调用新接口每切换一个模块就把原先直连老服务的调用点删除一个。最后等所有调用方都走了新接口老服务才被标记为废弃等待下个版本下线。这个过程中最值钱的教训有三条。第一条适配器一定放在消费者侧不要放在提供者侧。意思是让调用方去适配不要让老服务去适配新调用方否则每一家新系统接入都会逼着老服务改代码老服务最终会被改成四不像。第二条转换逻辑里尤其要注意空值、异常、时间格式、字符编码这四类边界XML转JSON时遇到null字段怎么处理、老接口返回超时怎么办、时间戳是秒还是毫秒、中文乱码问题这些都要在适配器里用单元测试锁死。第三条适配器类上写清楚“适配谁、为什么适配、什么时候可以删除”。很多老代码里的适配器没人敢删不是因为依赖还在而是因为没人知道它为什么存在。提示如果你发现一个适配器里开始出现if (type A) {} else if (type B) {}这类分支说明适配目标已经失控。尽快按“每种老类一个适配器”的原则拆分别让适配器变成“上帝翻译”。我个人在实际操作中的体会是适配器模式是最容易“无痛救火”的一个设计模式但它的陷阱也正在于此——太容易写了所以很容易被滥用。过度使用适配器会让系统里充满隐形的“两层皮”每加一层转换排查问题时就要多翻一层代码。所以判断适配器该不该上、该用继承还是组合比会写适配器本身重要得多。希望这篇文章能帮你把这点想明白。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑