1. 接口适配器模式:让不兼容的接口“握手”
做系统架构这些年,我遇到最多的一个场景就是:两套系统明明功能对得上,接口却死活接不上。就像一个是USB-A,一个是Type-C,中间缺根转接线。适配器模式,说白了就是这根“转接线”。
今天咱们就把它拆开看看,从原理到实战,把五种适配器形态一次讲透。
1.1 适配器模式原理
适配器模式的核心意图很简单:把一个类的接口转换成客户端期望的另一个接口。它解决的是“接口不匹配”的问题,而不是“功能缺失”的问题。
我习惯用一个公式来记:
三个角色缺一不可:
- 目标接口(Target):客户端期望的接口规范
- 被适配者(Adaptee):已经存在的、但接口不匹配的类
- 适配器(Adapter):中间人,负责转换调用
举个例子。你写了一个日志系统,统一用 log(message) 方法。但第三方库只提供 writeToFile(filename, content)。怎么办?写个适配器,把 log 调用转成 writeToFile 调用。就这么简单。
1.2 类适配器 vs 对象适配器
这是适配器模式里最经典的二选一。我当年刚入行时也纠结过,后来踩了坑才明白它们的适用场景。
| 对比维度 | 类适配器 | 对象适配器 |
|---|---|---|
| 实现方式 | 继承被适配者 + 实现目标接口 | 持有被适配者引用 + 实现目标接口 |
| 耦合度 | 高(继承是强耦合) | 低(组合是弱耦合) |
| 灵活性 | 低(只能适配一个被适配者) | 高(可适配多个被适配者) |
| 代码量 | 少 | 稍多 |
| 适用场景 | 被适配者接口固定,不需要额外行为 | 需要适配多个被适配者,或需要动态切换 |
类适配器示例(Java):
// 目标接口
public interface LogTarget {
void log(String message);
}
// 被适配者
public class FileLogger {
public void writeToFile(String filename, String content) {
// 写入文件逻辑
}
}
// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends FileLogger implements LogTarget {
@Override
public void log(String message) {
writeToFile("app.log", message);
}
}
对象适配器示例(Java):
// 对象适配器:持有被适配者引用
public class ObjectAdapter implements LogTarget {
private FileLogger fileLogger;
public ObjectAdapter(FileLogger fileLogger) {
this.fileLogger = fileLogger;
}
@Override
public void log(String message) {
fileLogger.writeToFile("app.log", message);
}
}
1.3 缺省适配器模式
你想想看,有时候一个接口定义了十几个方法,但你只需要实现其中一两个。每次都要写一堆空实现,烦不烦?
缺省适配器就是干这个的。它提供一个抽象类,把接口所有方法都实现成空方法或默认行为。你只需要继承这个抽象类,覆盖你关心的那几个方法就行。
// 一个有很多方法的接口
public interface EventListener {
void onClick();
void onDoubleClick();
void onRightClick();
void onHover();
void onDrag();
void onDrop();
}
// 缺省适配器:提供空实现
public abstract class DefaultEventListener implements EventListener {
@Override public void onClick() {}
@Override public void onDoubleClick() {}
@Override public void onRightClick() {}
@Override public void onHover() {}
@Override public void onDrag() {}
@Override public void onDrop() {}
}
// 使用时,只覆盖需要的方法
public class ClickListener extends DefaultEventListener {
@Override
public void onClick() {
System.out.println("点击事件处理");
}
}
我曾经在一个GUI框架里见过这种设计。框架定义了20多个事件回调,但大部分场景只需要处理点击和拖拽。缺省适配器让代码清爽了很多。
1.4 双向适配器
这个稍微进阶一点。双向适配器,就是适配器同时实现了两个接口,可以在两个方向上进行转换。
场景是这样的:系统A用接口A,系统B用接口B。有时候A要调用B,有时候B要调用A。写两个适配器?太麻烦。一个双向适配器搞定。
// 接口A
public interface InterfaceA {
void methodA();
}
// 接口B
public interface InterfaceB {
void methodB();
}
// 双向适配器
public class TwoWayAdapter implements InterfaceA, InterfaceB {
private InterfaceA targetA;
private InterfaceB targetB;
public TwoWayAdapter(InterfaceA a, InterfaceB b) {
this.targetA = a;
this.targetB = b;
}
@Override
public void methodA() {
targetB.methodB(); // A调用转成B调用
}
@Override
public void methodB() {
targetA.methodA(); // B调用转成A调用
}
}
1.5 适配器在接口兼容中的应用
讲完理论,咱们看看实际场景。适配器模式在接口兼容性处理中,几乎是标配。
场景一:新旧系统对接
老系统用SOAP,新系统用REST。写个适配器,把SOAP请求转成REST请求,再把REST响应转回SOAP响应。我在一个银行核心系统迁移项目中就这么干过,适配器层跑了两年,直到老系统完全下线。
场景二:第三方SDK集成
不同厂商的SDK接口千奇百怪。统一封装一层适配器,对外暴露标准接口。换厂商时,只需要换适配器实现,业务代码不用动。
场景三:多数据源统一
MySQL、Redis、MongoDB,查询方式各不相同。适配器模式可以把它们统一成 get(key)、set(key, value) 这样的通用接口。
下面这张图,是我做接口适配时常用的结构设计:
小结
适配器模式不是什么高深的技术,但它解决的是系统集成中最常见的问题。我个人习惯把它当作“接口翻译官”——两边语言不通,中间站个人翻译一下。
记住几个要点:
- 优先用对象适配器,组合比继承灵活
- 缺省适配器能省掉大量空实现代码
- 双向适配器小心循环调用
- 适配器层是系统边界,做好隔离,换实现时业务代码不用动