适配器设计模式:一个"翻译官"的故事
一句话记忆 :两个接口不兼容,中间加个翻译官;客户端只认目标口,适配器把旧货搬。
一、生活比喻:出国旅行带转换头
你带了一个国标插头 的充电器,到了英国,墙上只有英标插座。插不进去,怎么办?
你不需要把充电器拆了重做,也不需要把墙砸了重装,你只需要买一个转换头:
- 国标插头 → 转换头 → 英标插座
这个转换头就是适配器 。它做了一件很纯粹的事:把"我不认识的接口"翻译成"我认识的接口"。
再比如:
- 读卡器:SD 卡 → 读卡器 → USB 口
- 电源适配器:220V 交流 → 适配器 → 笔记本要的 19V 直流
- 同声传译:你说中文,老外只懂英文,翻译在中间来回转
它们的共同点都是:两边本来没法直接合作,加一层翻译就能合作了。
二、模式三角色:谁在适配谁?
适配器模式里有三个角色,记住这张表就够了:
| 角色 | 含义 | 插座比喻 |
|---|---|---|
| Target(目标接口) | 客户端期望使用的接口 | 墙上的英标插座 |
| Adaptee(被适配者) | 已经存在但接口不兼容的类 | 你的国标插头 |
| Adapter(适配器) | 把 Adaptee 翻译成 Target | 转换头 |
客户端只认 Target,它根本不知道 Adaptee 的存在。适配器在中间"左手拉一个、右手拉一个",把两边接上。
三、代码长什么样?
假设客户端只会调用 play(String audioType, String fileName) 这个目标接口,但手里只有一个只会 playMp4() 的老类。
java
// Target:客户端期望的接口
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee:已经存在、接口不兼容的老类
class Mp4Player {
public void playMp4(String fileName) {
System.out.println("播放 mp4:" + fileName);
}
}
// Adapter:翻译官,实现 Target,内部持有 Adaptee
class Mp4Adapter implements MediaPlayer {
private Mp4Player mp4Player = new Mp4Player();
@Override
public void play(String audioType, String fileName) {
if ("mp4".equalsIgnoreCase(audioType)) {
mp4Player.playMp4(fileName); // 翻译:play → playMp4
}
}
}
// 客户端只认 MediaPlayer
MediaPlayer player = new Mp4Adapter();
player.play("mp4", "变形金刚.mp4");
关键点只有一句:适配器实现目标接口,内部持有被适配者,在目标方法里调用被适配者的方法。
小知识:适配器有两种写法------类适配器 用继承(
extends Mp4Player implements MediaPlayer),对象适配器 用组合(上面这种)。实际开发中组合优于继承,对象适配器更常用。
四、SpringMVC 里它在哪?------HandlerAdapter
这是你学 SpringMVC 必须打通的一环。
问题背景 :DispatcherServlet 是前端控制器,它拿到请求后,通过 HandlerMapping 找到了一个"处理器"(Handler)。但问题是------Handler 的写法太多了:
- 实现
Controller接口的类 - 用
@Controller+@RequestMapping注解的方法 - 实现
HttpRequestHandler的类 - 甚至一个普通方法
DispatcherServlet 不可能为每一种写法都写一套 if-else 去调用,否则每加一种 Controller 写法就要改源码,违反开闭原则。
解决方案 :DispatcherServlet 只认一个统一接口------HandlerAdapter。
DispatcherServlet(客户端,只认 HandlerAdapter)
│
▼
HandlerAdapter(目标接口)
│
┌────┼──────────────┬────────────────────┐
▼ ▼ ▼ ▼
RequestMapping SimpleController HttpRequestHandler
HandlerAdapter HandlerAdapter Adapter
│ │ │
▼ ▼ ▼
@Controller 方法 实现 Controller 接口 HttpRequestHandler 实现类
每种 Handler 配一个专属 Adapter,Adapter 负责"怎么调用这个 Handler"。DispatcherServlet 只管调 adapter.handle(request, response, handler),具体怎么调,交给适配器。
这就是为什么 SpringMVC 能"无缝支持"各种 Controller 写法------新增一种 Handler,只需新增一个 HandlerAdapter,DispatcherServlet 一行不用改。
五、怎么记?给你三个记忆钩子
钩子 1:口诀
接口不匹配,中间加一层;
客户端认目标,适配器做翻译。
钩子 2:三问法
遇到任何"这算不算适配器"的场景,问三句:
- 谁和谁不兼容?(Target 和 Adaptee)
- 谁在中间翻译?(Adapter)
- 客户端是不是只认目标接口?(是 → 典型适配器)
钩子 3:SpringMVC 锚点
HandlerMapping 负责"找",HandlerAdapter 负责"调"。
找到的 Handler 千奇百怪,Adapter 负责把它们翻译成
DispatcherServlet认识的统一调用方式。
六、面试怎么答(30 秒版)
适配器模式用于解决接口不兼容 问题:将一个类的接口转换成客户端期望的另一个接口,使原本无法协同工作的类可以一起工作。它包含目标接口、被适配者和适配器三个角色,适配器通过组合 持有被适配者,在目标方法中转发调用。SpringMVC 中的
HandlerAdapter就是典型应用:DispatcherServlet只依赖HandlerAdapter接口,不同的 Handler 由对应的 Adapter 调用,从而在不修改DispatcherServlet的前提下支持多种 Controller 写法,体现了开闭原则 和解耦。
七、常见坑:别和这两个模式搞混
| 模式 | 核心目的 | 一句话区分 |
|---|---|---|
| 适配器 | 改接口,让不兼容变兼容 | "翻译官,只换说法不改内容" |
| 装饰器 | 加功能,接口不变 | "给咖啡加奶加糖,还是那杯咖啡" |
| 代理 | 控访问,接口不变 | "经纪人挡在你和明星之间" |
记忆点:适配器改变的是"接口形状",装饰器和代理改变的是"行为/访问",接口本身不变。
总结成一张卡片:
适配器 = 转换头 = 翻译官
目标接口 ← 适配器 → 被适配者
SpringMVC 锚点:DispatcherServlet 只认 HandlerAdapter,Handler 千变万化靠 Adapter 统一调用。
口诀:不匹配,加一层;认目标,做翻译。