说实话,设计模式这话题被写烂了,网上铺天盖地都是“策略模式”“观察者模式”的教程,但大多数人看的时候热血沸腾,看完合上电脑,遇到真实项目该不会用还是不会用。适配器模式尤其如此——它太“简单”了,简单到很多人觉得不就是写个转换类吗,有什么好学的?但恰恰是这种轻视,导致在实际系统里到处出现硬编码类型转换、接口互相咬合不上、一改需求就崩一片的惨状。我这篇不打算复读教科书,而是从一次真实对接经历讲起,把这个模式从定义、结构、代码到源码里的应用、再到容易混淆的边界,一层层掰开揉碎,最后给出可以直接落地的实操守则。如果你正在做系统集成、老代码改造、SDK封装,或者准备面试被问到“适配器模式怎么用”,这篇应该能帮到你。
1. 从一次真实对接说起:适配器模式解决什么痛点
1.1 新老代码不匹配的尴尬场景
先讲个我自己的经历。前几年接手一个老项目,内部有一套已经跑了七八年的日志系统,对外暴露的是Logger接口,方法名起得随性,什么logInfo、logError,参数还带个int level。后来公司统一技术栈,要求所有服务接入新的日志规范,新规范用的是Slf4j风格的Logger,接口方法变成了info(String msg)、error(String msg, Throwable t)。这下麻烦来了:老系统几十个类全都在调用旧接口,不可能让业务代码直接依赖新日志门面,那样改动量巨大,而且老代码里有些自定义的日志级别逻辑新门面根本不知道。
最粗暴的改法是什么?全局搜索替换,把老接口的调用全部改成新接口。但如果老接口和新接口方法签名不一样、参数类型不一样、异常处理逻辑不一样,替换完就是一遍遍编译报错,报一个改一个,改完还要担心改出运行时问题。我当时在代码评审会上看到有人真提了这样的方案,差点没背过气去。
1.2 适配器模式的核心:不修改原有类,让它适配新接口
这个场景的本质,是两套接口不兼容,而你又不能去改动其中任何一方的源码——老代码是存量资产,改不起;新规范是外部标准,动不了。适配器模式的解法非常朴素:在两者之间插入一个“翻译”,让老实现看起来就像新实现一样,或者反过来,让新调用方通过它能调用老实现。
用生活里的例子类比,你从国外带回来一个两脚插头的电器,家里墙上只有三孔插座,你会去砸墙重铺电路吗?不会。你只需要花几块钱买一个转换插头,一端插电器,另一端插插座,两头都识别不了你是“转换插头”,但电就是这么通了。适配器模式就是软件世界里的转换插头,它负责把某个类的接口转换成客户端期望的另一个接口。这个“协调者”屏蔽了两边的差异,让原本因为接口不匹配而无法协作的类能够一起工作。
1.3 为什么这个模式值得单独研究一遍
可能你会问,Java里的InputStreamReader不也是适配器吗?查一查API文档就完事了,哪里还需要专门写一篇来分析?这恰恰是我写这篇的原因。适配器模式在JDK里有、在Android源码里有、在Spring里有、在任何一个活得够久的系统里都有,但大家对它的理解大多停留在“见过、眼熟、能用”,真正问你“这个模式有几种实现方式”“类适配器和对象适配器分别适用什么场景”“适配器和外观模式有什么本质区别”,不少人又答不上来。这篇就是要解决这些“以为会其实没会”的问题,从角色拆到代码,从源码拆到实战边界,让下一次你遭遇接口不兼容的时候,第一反应不再是一股脑改老代码,而是先想想能不能加一个适配器。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四种角色与两种实现方向:先看结构再谈代码
2.1 角色拆解:Target、Adaptee、Adapter、Client
适配器模式的静态结构非常清晰,一共四个角色,我建议你先把这个模型印在脑子里,后面所有代码、所有源码分析都逃不出这个框架。
- Target(目标接口):客户端所期望的接口,也就是业务方其实想调用的那个“标准”。在日志场景里,新日志门面就是Target,业务代码希望对着它编程。
- Adaptee(被适配者):已经存在的、接口不兼容但功能正确的类,它就是那个“两脚插头的电器”。老日志系统的
Logger接口及其实现就是Adaptee。 - Adapter(适配器):核心角色,负责把Adaptee的接口转换成Target接口。客户端调用Target接口方法时,Adapter内部将这些调用转发给Adaptee的对应方法,必要时做参数转换、返回值包装、异常翻译。
- Client(客户端):通过Target接口与Adapter交互,完全不感知Adaptee的存在。
画一下依赖关系的话,就是这么个走向:Client依赖Target,Adapter实现Target,同时Adapter持有或继承Adaptee。注意这里面没有一个是多余的——Client不用知道Adaptee的细节,Adaptee也不必为满足Target做任何修改,Adapter是唯一一个需要感知双方的桥梁,所以理论上所有复杂度都被收拢到Adapter内部。
2.2 类适配器与对象适配器:Java与C++的玩法差异
适配器模式有两种经典实现,一种叫类适配器,另一种叫对象适配器。它们的区别从名字就能猜个大概:类适配器用继承(或接口实现)来“蹭”被适配者的能力,对象适配器用组合来持有被适配者的引用。
类适配器的结构是让Adapter同时继承Adaptee并实现Target接口(在Java里由于不能多继承,只能做接口实现,所以这里的“继承”实际是“继承一个类+实现一个接口”)。这样做的好处是可以直接调用Adaptee的protected方法,甚至覆盖它的行为,代码写起来直截了当,不需要额外维护一个内部对象。缺点是继承了Adaptee之后,Adapter和Adaptee就此绑定死了,如果Adaptee不是接口而是具体类,那适配器的复用性就受限了;而且继承会把Adaptee的私有状态也带进来,容易出现莫名其妙的状态干扰。
对象适配器则是Adapter持有Adaptee的引用,在实现Target接口的方法内部去调用Adaptee实例的方法。这种方式在Java、C++、Python里都更常用,因为组合优于继承是软件工程里的老生常谈。它的优点很明显:Adapter不依赖Adaptee的具体子类,任何Adaptee的子类或者代理对象都能传进来,耦合度更低,而且能适配Adaptee的整个子类体系。缺点是要多写一点样板代码,但这点代价和灵活性相比完全不值一提。
C++里类适配器可以体现得更彻底,因为C++支持多继承,可以让Adapter同时继承Target抽象类和Adaptee,直接获得两者的接口和实现。这种写法在C++里是合法的,阅读的时候要注意区分它和“多重继承滥用”的差别——适配器模式里的多继承目标明确,就是为了同时满足“接口一致”和“复用能力”两个诉求。
2.3 一个完整的Java代码实现:从接口到客户端全流程
回到日志那个例子,我把代码完整写出来。
先定义Target,也就是新规范要的接口:
java复制public interface NewLogger {
void info(String msg);
void error(String msg, Throwable t);
}
然后是Adaptee,也就是老的日志实现类。为了展示这个模式的独立性,我故意让老实现的写法很“老派”:
java复制public class OldLogger {
private int currentLevel;
public OldLogger(int level) {
this.currentLevel = level;
}
public void logInfo(int level, String msg) {
if (level <= currentLevel) {
System.out.println("[INFO] " + msg);
}
}
public void logError(int level, String msg, Throwable t) {
if (level <= currentLevel) {
System.out.println("[ERROR] " + msg);
if (t != null) {
t.printStackTrace();
}
}
}
}
注意,OldLogger根本不是一个接口,而是一个具体类。这种情况下类适配器只能选择继承它来写,但就像前面说的,继承具体类是高风险操作,所以我用对象适配器,把它包起来:
java复制public class LoggerAdapter implements NewLogger {
private final OldLogger oldLogger;
private static final int INFO_LEVEL = 1;
private static final int ERROR_LEVEL = 2;
public LoggerAdapter(OldLogger oldLogger) {
this.oldLogger = oldLogger;
}
@Override
public void info(String msg) {
oldLogger.logInfo(INFO_LEVEL, msg);
}
@Override
public void error(String msg, Throwable t) {
oldLogger.logError(ERROR_LEVEL, msg, t);
}
}
客户端就可以完全面向新规范编程,甚至不需要知道老实现的存在:
java复制public class Client {
private final NewLogger logger;
public Client(NewLogger logger) {
this.logger = logger;
}
public void doSomething() {
logger.info("start do something");
try {
// 业务逻辑
} catch (Exception e) {
logger.error("something failed", e);
}
}
}
// 组装
OldLogger old = new OldLogger(3);
NewLogger logger = new LoggerAdapter(old);
new Client(logger).doSomething();
这段代码的精髓在于:Client不依赖OldLogger,OldLogger也没有做任何修改,一切不兼容的细节都被LoggerAdapter吞掉了。以后即便老日志系统被彻底替换掉,我们只需要换一个NewLogger的实现类,Client一行都不用动。很多人做系统重构时抱怨改动范围太大,其实往往是因为没有在接口边界处插入适配层,而是让业务代码直接散落地依赖了具体实现。适配器模式教给我们的第一课,就是在变化的两端之间留出一个薄薄的中间层。
3. JDK与Android源码里那些适配器“名场面”
3.1 JDK中天天见却未必意识到的适配器案例
JDK里第一个值得说的是InputStreamReader。它是Reader的子类,目标接口是字符流读取,而它内部包装了一个InputStream,也就是字节流读取。为什么需要适配?文件、网络传进来的都是字节,而业务代码处理的时候希望按字符读,否则中文乱码问题足以把你折磨疯。InputStreamReader做的事就是把InputStream的read()字节语义翻译成Reader的read(char[] cbuf)字符语义,中间还要处理字符集解码。这是一个典型到教科书级别的对象适配器——大家天天在用,只是没意识到自己已经在享受适配器模式的福利。
第二个是java.util.Arrays#asList。它把数组适配成List接口,让原本只能通过下标访问的数组能使用集合框架的API。注意它返回的ArrayList并不是java.util.ArrayList,而是Arrays内部的一个私有静态类,这个适配器很“脆”——你不能对它调用add或remove,否则抛UnsupportedOperationException,因为它底层还是那个固定长度的数组。这提醒我们,适配器不一定保证百分百完整实现目标接口的每一个方法,对于不适用的方法,按需抛异常或降级处理都是可以被接受的,关键是你要在文档或代码注释里说清楚。
第三个是Collections.enumeration(Collection)和Collections.list(Enumeration),这两个互为逆向适配器。在老的遗留代码里,很多API返回的是Enumeration,而新代码习惯用Iterator,如果你去硬改对方,会引发大范围改动,但用这两个工具方法做一次适配,新老代码就能顺利握手。JDK的设计者显然在很久之前就想明白了这个道理:接口会变,但适配可以让变化不至于引发雪崩。
3.2 Android中Adapter的真实身份:桥梁还是适配器?
说到Android源码,不得不提的就是Adapter这个类族,比如ListAdapter、RecyclerView.Adapter。很多人刚学Android时搞不懂:为什么列表要配一个Adapter?不能直接传一个ArrayList进去让RecyclerView自己渲染吗?其实这里的设计动机非常符合适配器模式的经典场景:RecyclerView只负责“如何摆放、如何回收复用Item”这件事,它根本不应该关心你的数据是来自数据库、网络还是内存数组,因此它定义了一个Adapter目标接口,要求你实现getItemCount()、onCreateViewHolder()、onBindViewHolder()这几个方法。而你的自定义Adapter继承RecyclerView.Adapter时,实际上是在充当“数据源与视图之间的翻译官”:它把业务数据模型转成ViewHolder能绑定的视图,把数据集合的大小转成条目数量。每个Item的布局、数据绑定逻辑,全部收拢在Adapter内部。这恰恰是适配器模式的思想——两大组件(数据和视图)没有必要彼此认识,一个Adapter夹在中间负责两端方言的互译。
android.widget.ListView时代的BaseAdapter就更直白了,它甚至还有一个getView()方法,由系统在滚动时回调,让开发者把数据填充进convertView,这不就是“将任意数据集转换成View列表”的适配器么。所以很多人在面试时聊到Android的Adapter,会把它和适配器模式画等号,虽然严格说它更像一个“自定义接口实现”,并且掺杂了观察者模式(数据变化后notifyDataSetChanged通知视图刷新),但从模式的核心动机——解耦数据源和展示组件——看,它的确跑不出适配器模式的范畴。
3.3 JDK中另一个容易被忽略的适配器:ThreadPoolExecutor的Callable适配
还有一个容易被忽略的例子:Executors.callable(Runnable task)。它把没有返回值的Runnable适配成有返回值的Callable,返回的结果是null。ThreadPoolExecutor的submit方法支持接收Runnable和Callable,但如果有一堆Runnable任务你希望它们都能通过Future拿到执行完成信号,Executors.callable就是一个现成的适配器。这种“闲闲一笔”的小功能,往往是最能体现JDK设计者用心的地方:不要求你到处传Callable,而是允许你在需要的时候把已有类型适配过去。
4. 为什么说类适配器要慎用:继承破坏性的代价
4.1 Java单继承约束下的类适配器写法
虽然我一再强调对象适配器在绝大多数场景是首选,但为了知识的完整性,还是把类适配器的写法亮出来。假设上面那个OldLogger不是具体类,而是一个接口OldLoggerInterface,那类适配器可以这样写:
java复制public interface OldLoggerInterface {
void logInfo(int level, String msg);
void logError(int level, String msg, Throwable t);
}
public class OldLoggerImpl implements OldLoggerInterface {
public void logInfo(int level, String msg) { ... }
public void logError(int level, String msg, Throwable t) { ... }
}
public class LoggerClassAdapter extends OldLoggerImpl implements NewLogger {
public void info(String msg) {
logInfo(1, msg);
}
public void error(String msg, Throwable t) {
logError(2, msg, t);
}
}
看到问题没有?第一,当Adaptee是具体类时,你根本没法用类适配器去继承它,除非你愿意冒更大的继承风险。第二,即便Adaptee是接口,类适配器也强迫Adapter同时成为OldLoggerImpl的实例,这导致客户端如果拿OldLoggerImpl类型去接收适配器,就可以绕过NewLogger直接调用老方法——适配器的封装价值大打折扣。第三,类适配器无法适配那些被final修饰的类。所以在Java世界里,类适配器的使用场景非常苛刻,通常只出现在“你确实需要覆盖Adaptee的某个方法改变行为”的情况下。
4.2 对象适配器为何能成为事实标准
对象适配器最核心的一点,是适配器与被适配者之间是组合关系,而非继承关系。组合让适配器可以在构造函数里接收任意Adaptee的实例或子类,适配器只依赖Adaptee的公开接口,不触碰其内部状态。这样即使以后老日志系统换了一套完全不同的实现,你只需要在组装处换一个OldLogger实例,LoggerAdapter本身可以原封不动。这也符合设计模式里的“开闭原则”:对扩展开放,对修改关闭。你新增了一个适配器类,但没有修改任何既有类,所以系统整体变更风险被压低到一个新增类的范围内。
我在实际项目中还发现一个微妙的好处:对象适配器天然适合结合依赖注入。无论Spring还是Guice,你都可以方便地把Adaptee注入Adapter,再把Adapter作为Target接口的实现注入Client。控制反转容器最擅长的就是组装这种带依赖的对象图,而适配器模式正好为你提供了一个轻量的“转换层”Beans。如果用了类适配器,因为它是继承实现的,很多容器在代理、增强时反而会有些微妙的问题。所以我个人给你一条比较省心的建议——凡是遇到适配器模式,默认先写对象适配器,除非你有非常硬的理由需要覆盖Adaptee的非接口行为。
4.3 适配器模式在C++里的另一面:多继承的双刃剑
C++里适配器可以用多继承写,比如class Adapter : public Target, private Adaptee。这么做的好处是代码量极少,Adapter内部可以直接调用Adaptee的成员函数,不需要额外保存指针。但代价是,如果Target和Adaptee里出现了同名成员或虚函数,二义性就会冒出来,你还需要用using声明或全限定名去消歧。另外,Adaptee被私有继承时,它对外是不可见的,这确实保护了封装,但如果Target和Adaptee里都有虚函数且你希望Adapter能多态地替换其中某一个,这种写法就非常纠缠。C++社区后来的偏好也慢慢转向了组合优先,因为多继承带来的心智负担和维护成本往往超过了它节省的那点行数。如果你在用C++写适配器,我的建议和Java类似:优先组合,除非你要适配的Target和Adaptee都是抽象接口,而且你有绝对把握不出现命名冲突。
5. 适配器模式 vs 外观模式 vs 代理模式:三者的边界
5.1 核心意图不同:翻译、门面、控制
很多初学者会把适配器、外观、代理混在一起,因为三者都有一个“中间者”的感觉。但你抓住它们的核心意图就不会再乱。
- 适配器模式的核心意图是接口转换。它关心的是“让一个本来不能用的接口变得能用”,焦点在于兼容性。就像转换插头。
- 外观模式的核心意图是简化接口。它面对的可能是一堆已经能用的接口,但没有必要让客户端逐个认识它们,于是提供了一个更粗粒度的门面类,客户端只需要调门面的一个方法,内部自己编排一堆子系统的协作。焦点在于易用性,就像前台,你不需要知道背后是行政、财务还是技术。
- 代理模式的核心意图是控制访问。它保留原接口的完整形状,客户端甚至感知不到代理的存在,但代理可以在转发请求前后加入缓存、权限检查、延迟加载等逻辑。焦点在于控制,就像明星的经纪人,你找明星还是打那个电话,但电话那头接了之后做什么,由经纪人把关。
一句话概括:适配器是“改接口但不改功能”,外观是“合并接口但不新增功能”,代理是“原封不动接口但附加控制”。三者可以在同一个系统里共存,比如客户端对着外观编程,外观内部用适配器接入某个老系统,框架再为外观生成一个代理来控制并发访问——这不冲突,因为它们处在不同的作用和动机层级上。
5.2 为什么说适配器模式不一定非要“实现一个接口”
教科书上画适配器模式,通常画成Target是一个接口,Adapter implements Target。但实际项目里,Target不一定是接口,也可能是抽象类,甚至是一组方法签名约定。比如在某些脚本语言或动态类型语言里,根本没有接口这回事,适配器就是“长得像那么回事”的对象,只要能响应对应的方法名就行。Python中的鸭子类型就让适配器实现变得非常轻巧——你定义一个类,里面实现和Target相同的方法,然后在方法内部调用Adaptee。这种鸭子类型适配在Python、JavaScript里相当常见,它省去了“实现接口”的仪式感,但模式的思想一模一样:用一个新的对象去模拟客户端期望的形态,背后转发给真实实现。
5.3 适配器越用越多?可能你的架构出了问题
适配器模式是一把好用的刀,但如果你发现系统里适配器类爆炸式增长,那就要警惕了。这类情况通常是两种原因造成的:一是接入的第三方SDK或老系统太多,且彼此接口风格完全不同,这属于外部环境决定的,适配器多是自然现象;二是你自身内部的领域模型没有统一规范,各模块各写各的接口风格,导致模块之间大量需要适配,这就要反思是不是领域建模出了偏差,是不是缺少一层防腐层或统一API网关来做内部标准收敛。
经验法则:如果同一个Adaptee需要配上多个Adapter,那是正常的,比如Android里一个数据模型可能要适配成列表项展示和列表项滑动的不同视图;但如果同一个Target被反复适配,而且每次适配逻辑只有细微差别,那你更应该考虑抽出模板方法,或把变化的参数做成策略,而不是复制粘贴好几个几乎一样的适配器类。
6. 代码之外:写适配器时容易被忽略的几个坑与实操守则
6.1 坑一:适配器吞异常装透明,导致排查靠猜
适配器是做接口转换的,很多人下意识觉得所有异常都该在适配器里“翻译”一遍。但翻译归翻译,不能把异常吞掉。我在代码评审里见过有人这么写:
java复制try {
oldLogger.logError(level, msg, t);
} catch (Exception e) {
// ignore
}
理由是“老系统不稳定,我不想让新接口感知老系统的异常”。这种想法极其危险——适配器不是熔断器,它的职责是转换接口,不是掩盖故障。如果Adaptee本身抛了受检异常,而Target接口方法签名不允许抛,合理的做法是包装成运行时异常抛出去,或者在文档里明确告知调用方这个场景下可能出现的降级行为。把一个异常悄无声息吞掉,轻则让问题延迟暴露,重则掩盖了核心业务流程失败,线上排查的时候你只能看到一堆“看起来正常但结果不对”的诡异现象。适配器可以做参数翻译,但绝不能做事故隐藏器。
6.2 坑二:适配层过度设计,为了模式而模式
另一种常见问题是反向的:一上来不管有没有接口不兼容,先写一个Adapter层包着所有内部实现,美其名曰“为了将来扩展”。结果就是业务代码调用链越来越深:Controller -> Service -> XxxAdapter -> XxxServiceImpl,多了一层完全没有实际语义的间接层。适配器模式只有在确实存在两个不兼容的接口、且你无法改动其中一方的时候才引入。如果你自己既能改客户端又能改服务端接口,那最干净的做法是直接统一接口,而不是包一层适配器把不兼容掩盖住。毕竟任何间接层都意味着阅读理解成本和调用链路延迟,适配器是“不得不”的手段,而不是“锦上添花”的设计。
我个人的取舍标准是这样的:只有在这个接口变化是来自外部(第三方SDK版本升级、政府平台接口变化、老系统冻结维护)的时候,适配器才是我第一选择;如果双方都在自己掌控之内,我宁可花精力把两边的接口定义梳理成一致,也不愿意通过适配器长期维持一个“看起来统一其实两张皮”的状态。
6.3 实操建议:给Adapter划分出清晰的包名与命名约定
当一个系统里会出现很多适配器时,命名和包结构的管理就变得很重要。我的习惯是在某个adapter或integration包下面集中放这些类,命名统一采用XxxAdapter或XxxClientAdapter,让人一眼就知道这个类的存在是为了“适配某个外部组件”,而不是业务核心逻辑。同时,适配器里不要堆业务规则,它只做接口翻译和参数映射,一旦你在适配器里写了复杂的if-else业务判断,说明这个适配器已经开始“越权”了。
适配器类的代码注释里,我建议写清楚三件事:被适配的是谁(哪个类、哪个外部SDK)、目标接口是哪个、有哪些已知做不到的权衡(比如某方法无法保证原子性,或者某场景不支持取消)。这些信息是未来维护者的救命稻草,否则半年以后你看着一个PaymentAdapter,根本想不起当初它为什么要把createOrder映射成preCreate。
6.4 一个针对老系统改造时的具体注意点:事务与线程模型
最后说一个极容易被忽略的细节。如果Adaptee是数据库操作或远程调用,Adapter在转发时一定要留意事务边界和线程模型。比如新Target接口的方法是同步的,而Adaptee内部实现是异步的,你直接转发后,客户端会以为调用已经完成,其实任务还在队列里。这种语义错位比接口不兼容更难排查。我见过一个支付对接项目,适配器把新接口的pay()直接转发给老SDK的异步回调方法,结果客户端立刻收到“支付发起成功”,实际上老SDK还在等第三方回调,用户订单状态错乱,最后查了一个通宵才发现是适配器没有做异步转同步的桥接。所以在设计适配器方法时,你需要把调用语义也作为接口的一部分来适配,不只是参数签名。返回值到底代表“已受理”还是“已完成”,异常在什么粒度抛,这些语义如果两边不一致,适配器里必须做明确的调和,而不是简单透传。
7. 适配器模式的另一个维度:双向适配与多接口适配
7.1 双向适配器:让两套系统互相调用
大部分适配器是单向的,只负责帮Client去调Adaptee。但如果两个系统A和B各自维护了一套接口,它们之间有大量互相调用的需求,那就可以考虑双向适配器:一个类同时实现A接口和B接口,当从A视角看时,它是B的适配器,从B视角看时,它是A的适配器。这种实现直接把两个不兼容的系统桥接在一起,代价是Adapter内部需要持有双方的引用,逻辑复杂度和耦合度会同步上升。所以双向适配器只在系统整合初期、要快速让老系统和新系统互发消息时使用,一旦双方协同逻辑稳定了,还是应该收敛到统一接口规范上去。
7.2 多接口适配:实现多个Target接口的Adapter
还有一种情况,Adapter不需要只转换一个Target接口,它可以一次性实现多个接口,为不同客户端提供不同的视角。这在做设备接入平台时非常常见,一个硬件适配器,对于上层的监控模块是一个HealthCheckable,对于指令下发模块是一个CommandExecutor,对于数据上报模块是一个DataCollector。因为是同一个设备对象在背后支撑这三类能力,Adapter实现了三个接口,但内部转发给同一个设备会话。严格说这已经融合了适配器和门面两种特征,但理解的核心仍然是:每一个接口代表一种客户端视角,适配器负责让真实对象能满足这些视角的调用约定。
8. 写在最后:怎么判断自己真正掌握了适配器模式
说来说去,适配器模式既不复杂也不神秘,它做的无非是“翻译”两个字。判断自己是不是真掌握了,我觉得不看你能否背出UML图,而看你遇到下面两个场景时的反应。
第一个场景:产品让你对接一个第三方支付SDK,人家SDK里的接口是PaymentService.requestPayment(PaymentRequest request),你项目里现有支付门面是PayFacade.pay(Order order)。如果你第一反应是“把requestPayment封装进一个内部Service,把Order转换成PaymentRequest,然后老门面调这个内部Service”,你已经能把适配器用到实战里了。第二个场景:你在做一个开放平台,要接十几个外部相似但参数各异的API,如果你想到的不是写十几个平铺的adapter类,而是先抽象一个统一的ExternalApiClient目标接口,再给每个外部SDK写自己的Adapter,最后用工厂或注册表管理这些Adapter实例,那你不光会了适配器,还理解了适配器和工厂、策略这些模式之间如何配合。
我个人经历过很多次“深夜线上事故”之后最大的体会是:设计模式最值钱的地方不在于那几张类图,而在于它提供了一套大家都能理解的词汇和一个经过验证的思路。当你跟同事说“这里我加一个Adapter”,对方不需要看代码就能明白你是想在接口不兼容处做一个翻译层,仅这一点沟通效率的提升,就值回你学习这模式花的时间了。适配器模式是那种平时用起来不起眼、但遇到接口纠纷时最让人安心的小工具——就像包里常备的一个转换插头,不一定天天用,但真到了异国他乡的酒店床头,你会发现它比什么都管用。
