写软件的时候,你一定遇到过这种尴尬场景:你这边已经有一套很成熟、很稳定的接口,很多业务都在用,可是突然接入了一个"外部系统"或者"老代码模块",它的接口风格完全不一样,但功能本质上又差不多。你总不能为了配合它,把自己这边所有调用方都改一遍吧?那不仅工作量爆炸,而且极容易引入新 bug。
最直观的例子:你自己的项目里习惯用的是「5V 接口」,但外部提供的是「220V 电源」。你当然能直接把 220V 接到手机上,但是结果你也知道------要么烧掉,要么炸掉。这个时候,你需要的不是重写手机,而是一个"变压器"------它的职责是:一头接 220V,一头输出 5V,让双方都不用改变自己,就能愉快协作。
在代码世界里,这个"变压器",就是适配器模式中的「适配器对象」。
一、先看一个"硬接"导致的灾难现场
假设你在项目里已经有一个统一的日志接口,大家一直这么用:
public interface Logger {
void info(String msg);
void error(String msg);
}
你自己也实现了一个很简单的控制台日志:
public class ConsoleLogger implements Logger {
@Override
public void info(String msg) {
System.out.println("INFO: " + msg);
}
@Override
public void error(String msg) {
System.err.println("ERROR: " + msg);
}
}
业务代码里到处都是:
Logger logger = new ConsoleLogger();
logger.info("系统启动成功");
这时,leader 说要接入一个"第三方超级日志库",比如 ThirdPartyLog,它提供了更丰富的功能:按天分文件、支持异步刷盘、远程集中收集......但它的接口长这个样子:
public class ThirdPartyLog {
public void writeInfo(String tag, String content) {
// ...
}
public void writeError(String tag, String content, int code) {
// ...
}
}
你一看这接口就头大:
-
方法名完全不一样:writeInfo / writeError;
-
参数也不一致:多了 tag、错误码 code 等;
-
业务代码却已经全都写死使用 Logger.info / Logger.error。
如果你打算"硬改",无非两条路:
-
到处改业务:把所有 Logger 的调用替换成 ThirdPartyLog 的调用;
-
直接废弃 Logger 接口,重写相关模块。
leader 看你愁眉苦脸,笑着说:"别动业务代码,写个适配器就好了。"你这才开始正式接触适配器模式。
二、给旧接口和新接口之间,加一个"适配层"
leader 提醒你,适配器模式有三样典型角色:
-
目标接口(Target):客户端期望使用的接口(这里就是 Logger);
-
源(被适配者,Adaptee):已有的或第三方的接口(这里就是 ThirdPartyLog);
-
适配器(Adapter):一个中间人,让 Target 和 Adaptee 搭上线。
你的目标很明确:**不改业务调用 Logger 的方式,把 ThirdPartyLog"伪装"成一个 Logger。**
于是你写了这么一个适配器:
public class ThirdPartyLoggerAdapter implements Logger {
private ThirdPartyLog thirdPartyLog;
public ThirdPartyLoggerAdapter(ThirdPartyLog thirdPartyLog) {
this.thirdPartyLog = thirdPartyLog;
}
@Override
public void info(String msg) {
// 在这里"翻译"成第三方接口的调用
thirdPartyLog.writeInfo("default", msg);
}
@Override
public void error(String msg) {
thirdPartyLog.writeError("default", msg, -1);
}
}
现在业务代码可以完全不动,只是换了一个 Logger 的实现:
Logger logger = new ThirdPartyLoggerAdapter(new ThirdPartyLog());
logger.info("系统启动成功");
对业务而言:
-
它依然只认识 Logger;
-
完全不知道后面已经换成了一个复杂的第三方库;
-
所有"参数适配、方法名转换、默认值补齐"等脏活累活,全都交给适配器。
leader 看了之后很满意:"这就是典型的对象适配器。你只需要 understand 这三点:**不改老代码、封装转换逻辑、对外暴露原来那套接口**。"
三、类适配器和对象适配器的区别
写完上面的代码,你查资料时发现还有一个"类适配器"的概念,心里有点困惑:刚才写的不是已经可以用了么,为什么还要分两种?
leader 给你总结了一下:
-
**对象适配器**:适配器里"持有"一个 Adaptee 实例,通过组合来完成适配(你刚刚写的就是这个);
-
**类适配器**:适配器"继承"Adaptee,再实现 Target 接口,通过多重继承(在 Java 是"继承 + 实现接口")来完成适配。
比如刚才的例子,类适配器的写法大概是:
public class ThirdPartyLoggerClassAdapter extends ThirdPartyLog implements Logger {
@Override
public void info(String msg) {
writeInfo("default", msg);
}
@Override
public void error(String msg) {
writeError("default", msg, -1);
}
}
业务代码用起来也类似:
Logger logger = new ThirdPartyLoggerClassAdapter();
logger.info("系统启动成功");
区别在于:
-
类适配器通过继承拿到 Adaptee 的能力;
-
对象适配器通过组合拿到 Adaptee 的能力。
leader 通常会建议你:**在 Java 这种单继承语言里,优先使用对象适配器**,因为:
-
继承会让耦合更紧,适配器和具体实现绑死在一起;
-
组合更灵活,可以在运行时替换被适配对象,更利于扩展。
四、再看一个典型场景:List 和旧数组的互相适配
你在老项目里,可能有大量使用数组的老接口:
public class OldUserService {
public void saveUsers(String[] users) {
// ...
}
}
而新代码里,你早已习惯用 List:
List<String> users = new ArrayList<>();
users.add("张三");
users.add("李四");
你当然可以在每次调用前手动转换:
oldService.saveUsers(users.toArray(new String[0]));
可是一旦调用地方多了,这样的转换代码会到处乱飞,而且一旦 OldUserService 改接口,你要全局搜一遍再改,痛苦无比。
这个时候,你完全可以为"新世界"再包一层适配:
public interface NewUserService {
void saveUsers(List<String> users);
}
public class UserServiceAdapter implements NewUserService {
private OldUserService oldUserService;
public UserServiceAdapter(OldUserService oldUserService) {
this.oldUserService = oldUserService;
}
@Override
public void saveUsers(List<String> users) {
if (users == null) {
oldUserService.saveUsers(null);
return;
}
String[] array = users.toArray(new String[0]);
oldUserService.saveUsers(array);
}
}
之后你在新代码里统一使用 NewUserService 接口,所有"List -> 数组"的适配逻辑都集中在了 UserServiceAdapter 这一处。
leader 看完后,说了一句非常经典的话:"**能封装在一处的转换逻辑,就绝不要散落在各处。适配器就是帮你做这件事的。**"
五、适配器模式的正式定义
折腾了这么多例子,你终于可以给适配器模式下一个比较严谨的定义了:
适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
这里几个关键点:
-
"接口不兼容":功能类似,但方法签名、参数列表、命名等不一样;
-
"客户希望的接口":已有调用方已经依赖的一套接口约定;
-
"转换":通过适配器在中间做一层"翻译",双方互不修改,只加一个中间层。
六、适配器模式的优点和缺点
leader 照例让你总结优缺点,好让你知道什么时候用、什么时候别乱用。
优点:
-
**复用已有类**:可以在不修改源码的前提下,让旧类或第三方库融入现有系统。
-
**对客户端透明**:客户端只认识目标接口 Target,不需要知道背后真正调用的是谁。
-
**解耦转换逻辑**:所有"格式转换、参数补齐、调用顺序调整"都集中在适配器里,便于维护。
缺点:
-
**容易滥用**:如果系统设计一开始就很混乱,到处乱用适配器,最后会出现"适配器套适配器"的灾难。
-
**结构变复杂**:比直接调用多了一层间接,会让类的层次变厚一些。
-
**可能掩盖设计问题**:有时候接口不兼容本身是架构问题的信号,如果完全靠适配器去"打补丁",长期可能形成技术债。
leader 提醒你:"适配器更适合'兼容旧世界'或'对接第三方',不应该成为你日常设计新接口时的常规手段。"
七、适配器模式的典型应用场景
你再回头看自己的项目,发现适配器模式几乎无处不在:
-
**老系统迁移**:新系统用新接口,老系统还在用旧接口,中间加一层适配,做到"新老兼容"。
-
**第三方 SDK 封装**:对外暴露一套统一接口,内部通过各种适配器接不同厂商的 SDK。
-
**不同协议的统一接入**:比如 HTTP、MQ、WebSocket 等各种协议的消息,最终都适配成一套统一的内部事件接口。
-
**集合/流的类型转换封装**:比如把 Iterator 适配成 Enumeration,把数组适配成 List 等。
你这才真正理解 leader 那句话的含义:
当你发现"功能差不多,但接口风格完全对不上"时,不要急着改两边,更不要在业务代码里到处塞转换逻辑------先想想能不能在中间加一个适配器,让双方各自保持原样,只通过一个"翻译官"来对话。
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使原本由于接口不兼容而不能一起工作的类能够一起工作。