适配器模式:让不兼容的接口握手合作
适配器模式,说白了就是「转接头」。你想想看,Type-C 转 USB、三孔转两孔、HDMI 转 VGA——生活中到处都是适配器。代码里也一样,两个接口不匹配,中间加一层适配器,问题就解决了。
我最早接触这个模式是在做一个遗留系统重构的项目。老系统用 XML 传数据,新系统只认 JSON。不改老系统,也不能让新系统妥协,怎么办?加个适配器呗。嗯,这就是适配器模式的典型场景。
适配器模式的三种形态
适配器模式在 GoF 书里其实只分了两种:类适配器和对象适配器。但实际工作中,我习惯把「接口适配器」也单独拎出来讲,因为它在 Java 里太常用了。下面一个个说。
1. 类适配器(通过继承实现)
类适配器用多重继承来实现。Java 不支持多继承,所以类适配器在 Java 里用得少。C++ 倒是可以,但我个人不太推荐,因为继承耦合太强。
先看一个场景:我有一个 Volt220 类,输出 220V 电压。客户端需要 5V 电压。我需要一个适配器,把 220V 转成 5V。
// 目标接口:5V
public interface IVolt5 {
int output5V();
}
// 被适配者:220V
public class Volt220 {
public int output220V() {
return 220;
}
}
// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends Volt220 implements IVolt5 {
@Override
public int output5V() {
int volt = output220V();
// 降压处理
return volt / 44;
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
IVolt5 adapter = new ClassAdapter();
System.out.println("输出电压:" + adapter.output5V() + "V");
}
}
这段代码逻辑很简单。但注意,类适配器把 Volt220 的所有方法都继承下来了。如果 Volt220 还有其他方法,客户端也能调用到,这可能不是我们想要的。
2. 对象适配器(通过组合实现)
对象适配器用组合代替继承。适配器里持有被适配者的引用,调用时委托给它。这是我最推荐的方式,也是实际项目中最常见的。
// 对象适配器:持有被适配者,实现目标接口
public class ObjectAdapter implements IVolt5 {
private Volt220 volt220;
public ObjectAdapter(Volt220 volt220) {
this.volt220 = volt220;
}
@Override
public int output5V() {
int volt = volt220.output220V();
return volt / 44;
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
Volt220 volt220 = new Volt220();
IVolt5 adapter = new ObjectAdapter(volt220);
System.out.println("输出电压:" + adapter.output5V() + "V");
}
}
你看,对象适配器更灵活。被适配者可以运行时传入,甚至可以通过工厂方法动态选择不同的被适配者。我在项目中遇到过需要适配多种数据库的场景,用对象适配器,一个适配器类就能搞定 MySQL、Oracle、PostgreSQL 的差异。
3. 接口适配器(缺省适配模式)
接口适配器,也叫缺省适配模式。它的场景是这样的:一个接口定义了太多方法,但客户端只需要其中几个。你让客户端直接实现接口?那得写一堆空方法,太烦了。
解决方案:写一个抽象类,把接口所有方法都实现一遍(方法体为空)。客户端只需要继承这个抽象类,重写自己需要的方法即可。
// 一个有很多方法的接口
public interface IWorker {
void work();
void eat();
void sleep();
void rest();
void meeting();
}
// 接口适配器:空实现所有方法
public abstract class WorkerAdapter implements IWorker {
@Override
public void work() {}
@Override
public void eat() {}
@Override
public void sleep() {}
@Override
public void rest() {}
@Override
public void meeting() {}
}
// 客户端只需要实现 work 方法
public class Programmer extends WorkerAdapter {
@Override
public void work() {
System.out.println("写代码...");
}
}
Java 的 java.awt.event.MouseAdapter 就是典型的接口适配器。你写鼠标事件时,只需要继承 MouseAdapter,重写 mouseClicked 就行了,不用把 mousePressed、mouseReleased 等七八个方法全实现一遍。
Spring MVC 中的适配器模式
说到适配器模式的实际应用,Spring MVC 的 HandlerAdapter 是个经典案例。我当年看 Spring 源码时,看到这块忍不住拍了一下大腿——原来适配器还能这么用。
Spring MVC 里,处理器(Handler)有多种类型:
Controller接口(实现handleRequest方法)HttpRequestHandler接口(实现handleRequest方法,但参数不同)Servlet接口(实现service方法)- 注解驱动的
@RequestMapping方法
这些处理器的调用方式各不相同。如果没有适配器,DispatcherServlet 得写一堆 if-else 来判断处理器类型,然后分别调用。代码会变成这样:
if (handler instanceof Controller) {
((Controller) handler).handleRequest(request, response);
} else if (handler instanceof HttpRequestHandler) {
((HttpRequestHandler) handler).handleRequest(request, response);
} else if (handler instanceof Servlet) {
((Servlet) handler).service(request, response);
} // ... 越来越长
Spring 的做法是定义一个 HandlerAdapter 接口:
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
每种处理器对应一个适配器实现:
SimpleControllerHandlerAdapter—— 适配Controller接口HttpRequestHandlerAdapter—— 适配HttpRequestHandler接口SimpleServletHandlerAdapter—— 适配Servlet接口RequestMappingHandlerAdapter—— 适配@RequestMapping方法
DispatcherServlet 拿到处理器后,遍历所有 HandlerAdapter,找到 supports 返回 true 的那个,然后调用 handle 方法。整个过程完全符合开闭原则——新增一种处理器类型,只需要新增一个适配器实现,不需要改 DispatcherServlet。
三种适配器的对比
| 类型 | 实现方式 | 耦合度 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| 类适配器 | 继承被适配者 | 高(继承耦合) | 低(编译时确定) | 被适配者接口简单,需要继承其行为 |
| 对象适配器 | 组合被适配者 | 低(组合耦合) | 高(运行时动态组合) | 大多数场景,推荐使用 |
| 接口适配器 | 抽象类空实现 | 中 | 中 | 接口方法太多,客户端只需要部分方法 |
适配器模式的核心逻辑
下面这张图展示了适配器模式的核心结构。你可以看到,客户端只依赖目标接口,适配器在中间做转换,被适配者完全不知道适配器的存在。
什么时候用适配器模式?
我总结了几条经验,你写代码时可以对照着看:
- 想用现成的类,但它的接口和你的系统不匹配——加一层适配器,别改现成类的代码。
- 想创建一个可复用的类,它需要和多个未来可能出现的类协作——让这个类依赖目标接口,具体适配由适配器完成。
- 系统中存在多个功能相似但接口不同的类——用适配器把它们统一到同一个接口下,客户端代码就干净了。
好了,适配器模式就讲到这里。记住一句话:适配器不改变功能,只改变接口。下次遇到接口不兼容的问题,先想想能不能加个适配器,而不是去改别人的代码。