前两篇的代理和装饰器,包装对象是为了"加东西"------代理加控制、装饰器加功能。这一篇的适配器模式(Adapter) 也是包装,但目的完全不同:它包装一个对象,不是为了增强它,而是为了改变它的"接口长相",让原本对接不上的两个东西能协作起来。 一句话------适配器是个"转接头"。
这个比喻几乎不用解释。你的笔记本是 Type-C 口,酒店墙上是国标插座,中间插一个转接头,两边就通了。转接头没给你的电脑增加任何功能,它只干一件事:把一种接口形状,转换成另一种接口形状。 适配器模式在代码里干的就是这个------当你手上有一个现成的类(功能正合适),但它的方法签名/接口和你的系统要求的对不上时,你不去改它(可能也改不了,比如它是第三方库),而是写一个适配器夹在中间做转换。
这一篇我们用一个特别真实的场景:对接第三方物流 API 。你的系统里定义好了一套统一的物流接口,但顺丰、京东的 SDK 各有各的方法名和参数,跟你的接口完全对不上。适配器就是那个让它们能插进你系统的转接头。我们会讲清适配器的两种实现------对象适配器(靠组合) 和 类适配器(靠继承),以及它们的取舍;最后把"适配器、装饰器、代理"这三个长得很像的包装模式彻底辨析清楚,给结构型模式的"包装家族"做个小结。
这篇文章按这条线索展开:先摆出"接口对不上"的具体难题;再引出适配器这个转接头的思路;然后分别实现对象适配器和类适配器,比较两者;接着看标准库和 Spring 里的真实身影;最后把三个包装模式(适配器/装饰器/代理)放在一起辨析,并给出适配器的适用边界。贯穿例是物流对接。
目录
- 一个"接口对不上"的难题
- 适配器模式:夹在中间的转接头
- 对象适配器:靠组合做转换
- 类适配器:靠继承,以及两者的取舍
- [现实身影:标准库与 Spring 里的适配器](#现实身影:标准库与 Spring 里的适配器)
- [三个包装模式辨析:适配器 vs 装饰器 vs 代理](#三个包装模式辨析:适配器 vs 装饰器 vs 代理)
- 什么时候用适配器
一、一个"接口对不上"的难题
先看场景。我们的订单系统在发货时,需要调用物流下单。为了不和具体某家快递绑死,系统里定义了一个统一的物流接口,业务代码只面向它编程:
java
// 我方系统定义的统一物流接口
public interface LogisticsService {
String ship(String orderNo, String address); // 统一的下单方法
}
现在要接入顺丰。但顺丰给的 SDK 长这样------它是第三方的,你没法修改,而且方法名、参数和你的接口完全不一样:
java
// 顺丰 SDK(第三方,不可修改)
public class SfExpressSdk {
// 方法名、参数、返回值全都和 LogisticsService 对不上
public SfResult createOrder(SfRequest request) {
System.out.println("顺丰下单:" + request);
return new SfResult("SF" + System.currentTimeMillis());
}
}
问题就摆在这:你的业务代码想调 logisticsService.ship(orderNo, address),但顺丰只有 createOrder(SfRequest)。两者功能是匹配的 (都是物流下单),但接口形状对不上------方法名不同、参数结构不同、返回值不同。
有人会想:那我改业务代码,直接调顺丰的 createOrder 不就行了?这么做有两个大问题:其一,业务代码就和顺丰焊死了,以后换京东、加菜鸟,业务代码得跟着改一遍,违反了当初定义统一接口的初衷;其二,顺丰 SDK 你根本改不了,它是个 jar 包。
我们真正想要的是:让顺丰 SDK 能"伪装"成一个 LogisticsService,插进我们的系统,而业务代码和顺丰 SDK 都不用改。 中间需要一个转换层------这就是适配器。
二、适配器模式:夹在中间的转接头
适配器的思路直白得很:写一个新类,让它实现"我方要求的接口"(LogisticsService),在实现方法的内部,把调用翻译、转发给"被适配的对象"(顺丰 SDK)。 这个夹在中间做翻译的类,就是适配器。
它的三个角色:
- 目标接口(Target) :客户端期望的接口,我方的
LogisticsService; - 被适配者(Adaptee) :已存在的、接口不兼容的类,顺丰
SfExpressSdk; - 适配器(Adapter):实现 Target 接口,内部调用 Adaptee,完成两者之间的转换。
用一张图看清这个"转接头"夹在中间的位置:

图里最关键的是那条"翻译"路径:客户端 → 目标接口 → 适配器(翻译)→ 被适配者。客户端始终只跟目标接口打交道,压根不知道背后是顺丰还是京东;被适配者也保持原样,不知道自己被谁包了。适配器把"接口不匹配"这个脏活全揽在自己身上。
那么这个适配器具体怎么写?根据"适配器怎么持有被适配者"的方式不同,有两种实现:用组合 (对象适配器)和用继承(类适配器)。我们分别看。
三、对象适配器:靠组合做转换
对象适配器 :适配器持有一个被适配者的实例(组合),在实现目标接口方法时,调用这个实例并做参数/返回值的转换。
java
public class SfLogisticsAdapter implements LogisticsService {
private final SfExpressSdk sfSdk; // 组合:持有被适配者
public SfLogisticsAdapter(SfExpressSdk sfSdk) {
this.sfSdk = sfSdk;
}
@Override
public String ship(String orderNo, String address) {
// ------ 转换:把我方参数,翻译成顺丰要的格式 ------
SfRequest request = new SfRequest();
request.setBizOrderNo(orderNo);
request.setReceiverAddr(address);
SfResult result = sfSdk.createOrder(request); // 调用被适配者
// ------ 转换:把顺丰的返回,翻译成我方要的格式 ------
return result.getWaybillNo();
}
}
用起来,业务代码完全无感------它拿到的就是一个标准 LogisticsService:
java
LogisticsService logistics = new SfLogisticsAdapter(new SfExpressSdk());
String waybill = logistics.ship("NO123", "北京市朝阳区"); // 像调自家接口一样
好处很清楚:业务代码只依赖 LogisticsService,顺丰 SDK 原封不动,两者的差异被 SfLogisticsAdapter 这个转接头彻底吸收。以后要接京东,再写一个 JdLogisticsAdapter implements LogisticsService 就行,业务代码一个字不改------这又是开闭原则的体现。
对象适配器是实战中的首选,因为它靠组合,灵活性高:一个适配器可以适配被适配者及其子类;甚至一个适配器可以同时持有多个被适配者,把它们的能力组合起来对外提供。这也再次呼应了"组合优于继承"。
那还有一种"类适配器"是干嘛的?它换用继承来实现,有它的特点,也有明显的局限。
四、类适配器:靠继承,以及两者的取舍
类适配器 :适配器继承 被适配者、同时实现目标接口。这样它自己就"是"一个被适配者(继承来了它的方法),又对外表现为目标接口。
java
// 类适配器:继承被适配者 + 实现目标接口
public class SfLogisticsClassAdapter extends SfExpressSdk implements LogisticsService {
@Override
public String ship(String orderNo, String address) {
SfRequest request = new SfRequest();
request.setBizOrderNo(orderNo);
request.setReceiverAddr(address);
SfResult result = this.createOrder(request); // 直接调用继承来的方法
return result.getWaybillNo();
}
}
区别就在那个 this.createOrder(...)------因为继承了 SfExpressSdk,适配器可以直接调用父类方法,不用再持有一个实例。
但类适配器有一个 Java 特有的硬伤:Java 是单继承。 适配器一旦 extends SfExpressSdk,就用掉了它唯一的一次继承机会,再也没法继承别的类了。这意味着:
- 它只能适配
SfExpressSdk这一个具体类,没法像对象适配器那样灵活地适配一批相关的类; - 如果被适配者是
final类,它连继承都做不到,类适配器直接失效。
把两者对比一下:
| 对象适配器(组合) | 类适配器(继承) | |
|---|---|---|
| 持有被适配者的方式 | 组合(持有实例) | 继承(extends) |
| 灵活性 | 高,可适配子类、可组合多个 | 低,只能适配一个具体类 |
| 受单继承限制 | 否 | 是,用掉唯一继承名额 |
| 能否适配 final 类 | 能 | 不能 |
| 推荐度 | 首选 | 少用 |
结论很明确:优先用对象适配器。它靠组合,更灵活、限制更少,也符合"组合优于继承"。类适配器只在极少数场景(比如你确实需要重写被适配者的某些方法)才考虑,大多数时候用不上。你会发现,结构型模式一路走来,"组合优于继承"这条原则反复地在为我们做选择------代理、装饰器、适配器,能用组合的地方,组合几乎总是更好的那个答案。
五、现实身影:标准库与 Spring 里的适配器
适配器在标准库里非常常见,尤其在"新旧接口衔接""不同体系对接"的地方:
InputStreamReader:这是适配器的经典案例。InputStream是字节流 (一次读一个字节),Reader是字符流 (一次读一个字符),两者接口不兼容。InputStreamReader就是把InputStream适配成Reader的适配器------它持有一个InputStream(对象适配器),内部按指定字符集把字节转成字符。new InputStreamReader(inputStream, "UTF-8")这行你写过无数次的代码,就是在插一个"字节转字符"的转接头。Arrays.asList():把"数组"这个体系适配成"List"这个体系,让数组能用 List 的接口来访问。java.io里的各种XxxAdapter、Swing/AWT 的事件适配器 (如MouseAdapter,给你一个空实现好让你只重写关心的方法)。- Spring MVC 的
HandlerAdapter:这是框架级的精彩应用。Spring MVC 支持多种风格的 Controller(注解式@RequestMapping、实现Controller接口的、HttpRequestHandler的......),它们的调用方式各不相同。DispatcherServlet不可能为每种都写一套 if-else,于是引入HandlerAdapter------为每种 Controller 配一个适配器,DispatcherServlet只面向HandlerAdapter这个统一接口调用,由适配器把它翻译成对具体 Controller 的调用。这让 Spring MVC 能优雅地兼容多种 Controller 风格,是适配器"统一异构接口"能力的绝佳示范。
一个规律:凡是你看到"让一个老的/异构的/第三方的东西,能用我这套标准接口来调用"的地方,背后大概率就是适配器。
六、三个包装模式辨析:适配器 vs 装饰器 vs 代理
到这里,结构型模式里三个"长得像"的包装模式------代理、装饰器、适配器------都讲完了。它们的代码骨架确实相似(都持有一个对象、都对外提供方法、都在中间做点事),极易混淆。用一张表把它们的意图彻底分开,这是结构型"包装家族"最该记住的一张对照:
| 模式 | 一句话意图 | 接口是否改变 | 典型信号 |
|---|---|---|---|
| 代理 | 控制对对象的访问 | 不变(代理和真实对象同接口) | 加日志/权限/事务,你无感 |
| 装饰器 | 动态增强对象的功能 | 不变(装饰和被装饰同接口) | 层层叠加新能力,你主动包 |
| 适配器 | 转换对象的接口 | 改变(把 A 接口转成 B 接口) | 接口对不上,做个转接头 |
最关键、也最好用的一条区分判据是------看接口变没变:
- 代理和装饰器:接口不变。 它们包装前后,对外的接口是同一个 (都实现
Order),包装是"透明"的,调用方用起来和原来一样,区别只在"多了控制"或"多了功能"。 - 适配器:接口改变。 它的全部使命 就是"把接口 A 变成接口 B",输入一种接口,输出另一种接口。接口发生转换,是适配器区别于另外两个的根本标志。
再叠加上一篇那条"内部对象谁给的"判据,三者就彻底清晰了:接口变了 → 适配器;接口没变、你主动层层包着加功能 → 装饰器;接口没变、它替你挡在前面做管控 → 代理。 结构相似是表象,意图(尤其"接口变不变")才是它们各自的身份证。
七、什么时候用适配器
老规矩,泼冷水。适配器虽然实用,但它本质上是一种"补救"------它的存在,往往意味着系统里有接口不兼容的历史包袱。所以对它要有个清醒的态度。
适合用适配器的信号:
- 你想复用一个现成的类 ,它的功能正合适,但接口和你的系统对不上;
- 这个类你改不了(第三方库、遗留系统、别的团队的代码),或者不该改;
- 你需要让多个异构的实现(不同快递、不同 Controller 风格)统一到一套接口下。
要警惕的信号:
- 别把适配器当成"接口设计烂"的遮羞布。 如果两个接口本可以一开始就设计一致,却因为随意而对不上,那正确的做法是把接口设计好,而不是事后到处贴适配器。适配器是用来对接"你无法控制的外部",而不是用来给"你自己能控制的内部混乱"打补丁。
- 如果一个系统里适配器满天飞,那通常是个信号:接口抽象出了问题,该回头审视设计,而不是继续加转接头。
一句话:适配器是接纳"既成事实的不兼容"的优雅方案,但不是纵容"本可避免的不兼容"的借口。 用它来对接外部世界,而不是掩盖内部的设计债。
小结 。适配器模式是个"转接头":当现成的类功能合适、但接口对不上时,它夹在中间做接口转换,让两者协作,而双方都不用改。它有对象适配器 (靠组合,首选)和类适配器 (靠继承,受单继承所限,少用)两种实现------又一次,"组合优于继承"帮我们做了选择。InputStreamReader(字节流转字符流)、Arrays.asList、Spring MVC 的 HandlerAdapter,都是它的经典身影。而它和代理、装饰器最本质的区别,就一条:适配器改变接口,另外两个不改 。至此,结构型的三个"包装模式"齐了。下一篇我们转向另一类结构型------不再是"包装一个对象",而是"组织一群对象 ":当你面对一个树形结构(比如订单里嵌套着套餐、套餐里又嵌着子商品)时,组合模式能让你用统一的方式处理"单个"和"一组"。