Appearance
适配器模式 (Adapter Pattern)
一、定义
一句话概括:将一个类的接口转换成客户端期望的另一个接口,使原本不兼容的类能够一起工作。
官方定义(GoF):Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
适配器模式就像现实生活中的电源适配器——中国的插座是 220V 三孔,美国的笔记本是 110V 两孔,中间需要一个"转接头"来适配。在软件工程中,适配器模式起到同样的"桥梁"作用。
二、解决的问题
2.1 什么场景下需要适配器模式?
- 集成遗留系统:旧系统提供了稳定的功能,但其接口与新系统不兼容,不能修改旧代码。
- 第三方库封装:引入第三方 SDK 时,其接口风格与项目现有代码风格不一致,需要统一适配。
- 多数据源统一:系统需要同时对接 MySQL、Redis、Elasticsearch 等多种数据源,客户端希望使用统一接口。
- 接口升级兼容:系统升级后,旧的 API 调用方仍需要兼容,但新接口签名已变化。
2.2 不用适配器模式会有什么问题?
问题1:代码耦合 —— 客户端代码直接依赖第三方接口,切换成本高
问题2:代码重复 —— 每个调用方都要自己写转换逻辑,大量重复代码
问题3:难以维护 —— 第三方库升级后接口变化,所有调用方都需要修改
问题4:不符合开闭原则 —— 无法在不修改客户端代码的情况下扩展新接口三、结构
3.1 文字描述
适配器模式包含以下角色:
- Target(目标接口):客户端期望使用的接口。
- Adaptee(被适配者):已经存在的、需要被适配的类或接口。
- Adapter(适配器):实现 Target 接口,内部持有 Adaptee 引用,将 Target 的调用转换为 Adaptee 的调用。
3.2 对象适配器(推荐)
┌─────────────────┐ ┌─────────────────────┐
│ <<interface>> │ │ Adaptee │
│ Target │ ├─────────────────────┤
├─────────────────┤ │ + specificRequest() │
│ + request() │ └──────────┬──────────┘
└────────┬────────┘ │
│ │ adaptee
│ ┌───────────────┴───────────┐
│ │ Adapter │
│ ├───────────────────────────┤
└─────────────>│ - adaptee: Adaptee │
│ + request() │
│ adaptee.specificRequest()│
└───────────────────────────┘3.3 类适配器(多重继承)
┌─────────────────┐ ┌─────────────────────┐
│ <<interface>> │ │ Adaptee │
│ Target │ ├─────────────────────┤
├─────────────────┤ │ + specificRequest() │
│ + request() │ └──────────┬──────────┘
└────────┬────────┘ │
│ ┌───────────────────┴───────────┐
│ │ Adapter (extends Adaptee) │
└─────────>│ + request() │
│ this.specificRequest() │
└────────────────────────────────┘四、代码实现
4.1 基础实现
场景描述
假设我们有一个老旧的日志系统(LegacyLogger),它的接口是 logMessage(String msg)。现在新系统希望使用统一的 Logger 接口,该接口定义了 info()、error()、debug() 等方法。
java
// ============ Target:客户端期望的接口 ============
interface Logger {
void info(String message);
void error(String message);
void debug(String message);
}
// ============ Adaptee:已有的老旧日志实现 ============
class LegacyLogger {
public void logMessage(String level, String message) {
System.out.println("[" + level + "] " + message);
}
}
// ============ Adapter:对象适配器 ============
class LoggerAdapter implements Logger {
private LegacyLogger legacyLogger;
public LoggerAdapter(LegacyLogger legacyLogger) {
this.legacyLogger = legacyLogger;
}
@Override
public void info(String message) {
legacyLogger.logMessage("INFO", message);
}
@Override
public void error(String message) {
legacyLogger.logMessage("ERROR", message);
}
@Override
public void debug(String message) {
legacyLogger.logMessage("DEBUG", message);
}
}
// ============ 客户端测试 ============
public class AdapterDemo {
public static void main(String[] args) {
LegacyLogger legacy = new LegacyLogger();
Logger logger = new LoggerAdapter(legacy);
logger.info("用户登录成功");
logger.error("数据库连接失败");
logger.debug("查询参数: userId=123");
}
}4.2 进阶实现
4.2.1 类适配器 vs 对象适配器
对象适配器(推荐):使用组合方式持有 Adaptee 引用。
java
// 对象适配器 —— 灵活,可以适配 Adaptee 及其子类
class ObjectAdapter implements Target {
private Adaptee adaptee; // 组合
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest(); // 委托调用
}
}类适配器:通过继承 Adaptee 并实现 Target 接口。
java
// 类适配器 —— 只能适配一个具体类,但可以重写 Adaptee 的行为
class ClassAdapter extends Adaptee implements Target {
@Override
public void request() {
super.specificRequest(); // 直接调用父类方法
}
}
// 对比:
// 对象适配器优点:可以适配一个类及其所有子类,更灵活,符合"组合优于继承"
// 类适配器优点: 不需要重新实现整个 Adaptee,可以重写部分行为
// 结论:Java 中推荐使用对象适配器(因为 Java 不支持多继承)4.2.2 双向适配器
当两个系统需要互相调用,但接口不兼容时,可以使用双向适配器。
java
// 新系统接口
interface NewService {
String fetchData(String key);
}
// 旧系统接口
interface OldService {
String getRecord(int id);
}
// 双向适配器
class TwoWayAdapter implements NewService, OldService {
private NewService newService;
private OldService oldService;
public TwoWayAdapter(NewService newService, OldService oldService) {
this.newService = newService;
this.oldService = oldService;
}
// 实现 NewService,调用 OldService
@Override
public String fetchData(String key) {
int id = Integer.parseInt(key);
return oldService.getRecord(id);
}
// 实现 OldService,调用 NewService
@Override
public String getRecord(int id) {
return newService.fetchData(String.valueOf(id));
}
}4.2.3 默认适配器(接口适配器)
当一个接口方法过多,而我们只想实现其中一部分时,可以先用一个抽象类空实现所有方法(默认适配器),再用子类重写需要的方法。
java
// 大量方法的接口
interface MouseListener {
void onClick();
void onDoubleClick();
void onRightClick();
void onDrag();
void onDrop();
void onHover();
}
// 默认适配器:空实现所有方法
abstract class MouseAdapter implements MouseListener {
@Override public void onClick() {}
@Override public void onDoubleClick() {}
@Override public void onRightClick() {}
@Override public void onDrag() {}
@Override public void onDrop() {}
@Override public void onHover() {}
}
// 客户端只需重写关心的方法
class MyMouseHandler extends MouseAdapter {
@Override
public void onClick() {
System.out.println("鼠标点击!");
}
}4.3 生产级实现
Spring Boot 多支付渠道适配
java
// ============ 统一支付接口(Target)============
public interface PaymentService {
PaymentResult pay(PaymentRequest request);
PaymentResult refund(String orderId);
PaymentStatus queryStatus(String orderId);
}
// ============ 支付宝 SDK(Adaptee 1)============
class AlipaySDK {
public String createOrder(String amount, String subject) {
System.out.println("支付宝创建订单: " + amount + " - " + subject);
return "ALIPAY_" + System.currentTimeMillis();
}
public boolean doRefund(String tradeNo) {
System.out.println("支付宝退款: " + tradeNo);
return true;
}
public String queryTrade(String tradeNo) {
return "SUCCESS";
}
}
// ============ 微信支付 SDK(Adaptee 2)============
class WechatPaySDK {
public String unifiedOrder(int totalFee, String body) {
System.out.println("微信统一下单: " + totalFee + " - " + body);
return "WX_" + System.currentTimeMillis();
}
public boolean refund(String outTradeNo) {
System.out.println("微信退款: " + outTradeNo);
return true;
}
public String orderQuery(String outTradeNo) {
return "SUCCESS";
}
}
// ============ 适配器 ============
@Component
class AlipayAdapter implements PaymentService {
private final AlipaySDK alipaySDK;
public AlipayAdapter(AlipaySDK alipaySDK) {
this.alipaySDK = alipaySDK;
}
@Override
public PaymentResult pay(PaymentRequest request) {
String tradeNo = alipaySDK.createOrder(
request.getAmount().toString(), request.getSubject());
return new PaymentResult(true, tradeNo);
}
@Override
public PaymentResult refund(String orderId) {
boolean success = alipaySDK.doRefund(orderId);
return new PaymentResult(success, orderId);
}
@Override
public PaymentStatus queryStatus(String orderId) {
String status = alipaySDK.queryTrade(orderId);
return PaymentStatus.valueOf(status);
}
}
@Component
class WechatPayAdapter implements PaymentService {
private final WechatPaySDK wechatPaySDK;
public WechatPayAdapter(WechatPaySDK wechatPaySDK) {
this.wechatPaySDK = wechatPaySDK;
}
@Override
public PaymentResult pay(PaymentRequest request) {
String tradeNo = wechatPaySDK.unifiedOrder(
request.getAmount().intValue(), request.getSubject());
return new PaymentResult(true, tradeNo);
}
@Override
public PaymentResult refund(String orderId) {
boolean success = wechatPaySDK.refund(orderId);
return new PaymentResult(success, orderId);
}
@Override
public PaymentStatus queryStatus(String orderId) {
String status = wechatPaySDK.orderQuery(orderId);
return PaymentStatus.valueOf(status);
}
}
// ============ 辅助类 ============
class PaymentRequest {
private BigDecimal amount;
private String subject;
// getters & setters...
public BigDecimal getAmount() { return amount; }
public String getSubject() { return subject; }
}
class PaymentResult {
private boolean success;
private String tradeNo;
public PaymentResult(boolean success, String tradeNo) {
this.success = success;
this.tradeNo = tradeNo;
}
}
enum PaymentStatus { SUCCESS, FAIL, PROCESSING }
// ============ 策略模式选择适配器 ============
@Service
class PaymentGateway {
private final Map<String, PaymentService> paymentMap;
// Spring 自动注入所有 PaymentService 实现
public PaymentGateway(List<PaymentService> paymentServices) {
paymentMap = new HashMap<>();
for (PaymentService ps : paymentServices) {
String name = ps.getClass().getSimpleName();
paymentMap.put(name, ps);
}
}
public PaymentResult pay(String channel, PaymentRequest request) {
PaymentService service = paymentMap.get(channel + "Adapter");
if (service == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
return service.pay(request);
}
}五、优缺点
优点
| 优点 | 说明 |
|---|---|
| 解耦 | 将客户端与具体实现分离,客户端只依赖目标接口 |
| 复用 | 无需修改现有代码即可复用已有功能 |
| 灵活性 | 可以随时替换被适配者,不影响客户端 |
| 符合开闭原则 | 新增适配器无需修改现有代码 |
缺点
| 缺点 | 说明 |
|---|---|
| 复杂度增加 | 引入额外的类和接口,系统复杂度上升 |
| 性能开销 | 多一层间接调用,有轻微性能损失 |
| 过度设计风险 | 对于简单场景,直接修改被适配者可能更高效 |
六、适用场景
- 系统集成:对接第三方服务(支付、短信、推送),统一内部接口。
- 遗留系统改造:老系统接口与新系统不兼容,不能修改老代码。
- 数据格式转换:不同数据源(XML/JSON/CSV)需要统一为相同格式输出。
- 多版本兼容:同一个功能需要支持多个版本的 API。
- 硬件驱动适配:不同厂商的硬件设备需要统一的操作接口。
七、JDK / Spring 框架中的实际应用
7.1 JDK 中的适配器
java
// 1. Arrays.asList() —— 数组到 List 的适配
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr); // 数组适配为 List
// 2. InputStreamReader —— 字节流到字符流的适配
InputStream byteStream = new FileInputStream("file.txt");
Reader charStream = new InputStreamReader(byteStream, "UTF-8");
// 3. Collections.enumeration() / Collections.list() —— 双向适配
Enumeration<String> enumeration = Collections.enumeration(list);
List<String> backToList = Collections.list(enumeration);7.2 Spring 中的适配器
java
// 1. HandlerAdapter —— Spring MVC 的处理器适配器
// DispatcherServlet 通过 HandlerAdapter 适配各种 Controller
// HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter 等
// 2. JpaVendorAdapter —— JPA 厂商适配
// HibernateJpaVendorAdapter、EclipseLinkJpaVendorAdapter 等
// 3. AdvisorAdapter —— AOP 通知适配器
// 将不同类型的 Advice 适配为统一的 MethodInterceptor八、与其他模式的关系
| 模式 | 关系 |
|---|---|
| 桥接模式 | 桥接是预先设计(分离抽象与实现),适配器是事后补救(接口不兼容)。桥接更强调"独立变化",适配器更强调"兼容转换"。 |
| 装饰器模式 | 装饰器不改变接口,只增强功能;适配器改变接口,不改变功能。 |
| 外观模式 | 外观定义新接口简化子系统;适配器复用已有接口进行转换。 |
| 代理模式 | 代理和原对象实现相同接口;适配器和目标对象接口不同。 |
| 策略模式 | 常与适配器组合——策略选择不同适配器,实现多渠道统一调用。 |
九、面试常见问题
Q1:类适配器和对象适配器有什么区别?各有什么优缺点?
答:
- 类适配器使用继承,直接继承 Adaptee 并实现 Target 接口。优点是可以重写 Adaptee 的行为,不需要重新实现整个 Adaptee;缺点是只能适配一个具体类(Java 单继承限制)。
- 对象适配器使用组合,持有 Adaptee 引用。优点是更灵活,可以适配一个类及其所有子类,符合"组合优于继承"原则;缺点是不能重写 Adaptee 的行为。
- Java 中推荐对象适配器。
Q2:适配器模式和装饰器模式有什么区别?
答:
- 目的不同:适配器改变接口,让不兼容的接口协同工作;装饰器不改变接口,给对象动态添加新功能。
- 关注点不同:适配器关注"接口转换";装饰器关注"功能增强"。
- 典型例子:
InputStreamReader是适配器(字节→字符);BufferedInputStream是装饰器(给字节流加缓冲)。
Q3:适配器模式和外观模式有什么区别?
答:
- 适配器用于复用已有接口,将一个接口转换为另一个接口。
- 外观模式用于简化接口,为复杂子系统提供统一的高层接口。
- 适配器通常在事后使用(补救),外观模式通常在事前设计(规划)。
Q4:如何在 Spring Boot 中实现多支付渠道的适配器模式?
答:定义统一的 PaymentService 接口,为每个支付渠道(支付宝、微信、银联)创建独立的 Adapter 实现类。通过 @Component 注册,利用 Spring 的依赖注入将所有实现收集到 Map<String, PaymentService> 中,根据渠道名称动态选择对应的适配器。
Q5:双向适配器是什么?什么场景使用?
答:双向适配器同时实现两个接口,让两个系统可以互相调用。典型场景是两个遗留系统需要集成,但各自的接口不兼容,且双方都不能修改。双向适配器通过两种实现方式(implements A, B)将两个方向的调用相互转换。
