适配器模式(Adapter Pattern)¶
一句话记忆口诀:适配器转换接口,让不兼容的类协同工作,
Arrays.asList()和 Spring MVCHandlerAdapter是最熟悉的例子。
1. 引入:它解决了什么问题?¶
没有适配器模式时的问题¶
当需要使用一个已有的类,但其接口与当前系统要求的接口不匹配时:
// ❌ 反例:第三方支付库的接口与系统要求不匹配
// 系统要求的支付接口
public interface PaymentGateway {
boolean charge(String userId, double amount, String currency);
}
// 第三方支付库(无法修改)
public class StripePaymentSDK {
public PaymentResult processPayment(PaymentRequest request) {
// Stripe 的支付逻辑
return new PaymentResult(true, "txn_123");
}
}
// ❌ 直接使用:调用方必须了解 Stripe SDK 的细节,与第三方强耦合
public class OrderService {
private StripePaymentSDK stripe = new StripePaymentSDK();
public void pay(String userId, double amount) {
PaymentRequest req = new PaymentRequest(userId, amount, "USD");
PaymentResult result = stripe.processPayment(req); // 直接依赖第三方
// 如果换成 PayPal,这里所有代码都要改!
}
}
问题根因:调用方与第三方实现强耦合,更换实现时需要修改大量代码。
工作中的典型应用场景¶
| 场景 | Spring/JDK 中的例子 |
|---|---|
| 数组转集合 | Arrays.asList(array) |
| Spring MVC 请求处理 | HandlerAdapter 适配不同类型的 Handler |
| SLF4J 日志门面 | 适配 Log4j、Logback 等不同实现 |
| 旧系统集成 | 将旧 API 适配为新接口 |
| 第三方 SDK 集成 | 将第三方 SDK 适配为系统内部接口 |
2. 类比:用生活模型建立直觉¶
生活类比:电源适配器¶
出国旅行时,中国的两孔插头(被适配者)无法直接插入美国的三孔插座(目标接口)。电源适配器(Adapter)解决了这个问题:一端接中国插头,另一端符合美国插座规格。
- 接口/抽象角色:美国插座规格(
USSocket接口),定义三孔插入行为 - 被适配者:中国插头(
ChinaPlug),有自己的接口但与目标不兼容 - 适配器角色:电源适配器(
PowerAdapter),实现目标接口,内部调用被适配者 - 调用方:美国电器(
Client),只认识美国插座规格
抽象定义¶
适配器模式将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
3. 原理:逐步拆解核心机制¶
UML 类图(对象适配器 vs 类适配器)¶
classDiagram
class Target {
<<interface>>
+request() void
}
class Adaptee {
+specificRequest() void
}
class ObjectAdapter {
-adaptee Adaptee
+request() void
}
class ClassAdapter {
+request() void
}
class Client
Target <|.. ObjectAdapter
Target <|.. ClassAdapter
ObjectAdapter --> Adaptee : 组合(推荐)
ClassAdapter --|> Adaptee : 继承(不推荐)
Client --> Target
note for ObjectAdapter "对象适配器:组合方式<br/>更灵活,可适配子类"
note for ClassAdapter "类适配器:继承方式<br/>Java 单继承限制,不推荐"
Java 代码示例¶
// ===== 目标接口(系统期望的接口)=====
public interface PaymentGateway {
boolean charge(String userId, double amount, String currency);
}
// ===== 被适配者(第三方 SDK,无法修改)=====
public class StripePaymentSDK {
public PaymentResult processPayment(PaymentRequest request) {
System.out.println("Stripe 处理支付: " + request.getAmount());
return new PaymentResult(true, "stripe_txn_" + System.currentTimeMillis());
}
}
public class PayPalSDK {
public boolean executePayment(String payerId, double amount) {
System.out.println("PayPal 处理支付: " + amount);
return true;
}
}
// ===== 对象适配器(推荐:组合方式)=====
// 设计原因:通过组合持有被适配者,比继承更灵活(可以适配被适配者的子类)
// 代价:需要实现目标接口的所有方法
public class StripeAdapter implements PaymentGateway {
private final StripePaymentSDK stripe; // 持有被适配者的引用
public StripeAdapter(StripePaymentSDK stripe) {
this.stripe = stripe;
}
@Override
public boolean charge(String userId, double amount, String currency) {
// 接口转换:将目标接口的参数转换为被适配者的参数格式
PaymentRequest request = new PaymentRequest(userId, amount, currency);
PaymentResult result = stripe.processPayment(request);
return result.isSuccess();
}
}
public class PayPalAdapter implements PaymentGateway {
private final PayPalSDK paypal;
public PayPalAdapter(PayPalSDK paypal) {
this.paypal = paypal;
}
@Override
public boolean charge(String userId, double amount, String currency) {
// PayPal 不支持多币种,这里做转换处理
double convertedAmount = convertCurrency(amount, currency, "USD");
return paypal.executePayment(userId, convertedAmount);
}
private double convertCurrency(double amount, String from, String to) {
// 货币转换逻辑
return amount; // 简化示例
}
}
// ===== 调用方(只依赖目标接口,不依赖具体实现)=====
public class OrderService {
private final PaymentGateway paymentGateway; // 只依赖接口
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void processOrder(String userId, double amount) {
boolean success = paymentGateway.charge(userId, amount, "CNY");
if (success) {
System.out.println("订单支付成功");
}
}
}
// ===== 使用示例(切换支付方式只需换适配器)=====
public class Main {
public static void main(String[] args) {
// 使用 Stripe
OrderService stripeOrder = new OrderService(
new StripeAdapter(new StripePaymentSDK()));
stripeOrder.processOrder("user_001", 100.0);
// 切换到 PayPal,OrderService 代码不变!
OrderService paypalOrder = new OrderService(
new PayPalAdapter(new PayPalSDK()));
paypalOrder.processOrder("user_001", 100.0);
}
}
Spring MVC HandlerAdapter 示例¶
// Spring MVC 中,HandlerAdapter 适配不同类型的 Handler
// 目标接口
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
}
// 适配 @Controller 注解的方法
public class RequestMappingHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod; // 支持注解方式的 Handler
}
// ...
}
// 适配实现 Controller 接口的旧式 Handler
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof Controller; // 支持接口方式的 Handler
}
// ...
}
核心流程图¶
flowchart LR
A[Client] -->|调用 target.request| B[Adapter]
B -->|转换参数格式| C[Adaptee]
C -->|返回结果| B
B -->|转换返回值格式| A
style B fill:#ff9,stroke:#333
note1["适配器负责<br/>接口转换和参数转换"]
4. 特性:关键对比¶
对象适配器 vs 类适配器¶
| 对比维度 | 对象适配器(组合) | 类适配器(继承) |
|---|---|---|
| 实现方式 | 持有被适配者的引用 | 继承被适配者 |
| 灵活性 | ✅ 可适配被适配者的子类 | ❌ 只能适配特定类 |
| Java 限制 | 无 | Java 单继承,无法同时继承多个类 |
| 推荐度 | ✅ 推荐 | ❌ 不推荐(Java 中) |
适配器模式 vs 外观模式(Facade)¶
| 对比维度 | 适配器模式 | 外观模式 |
|---|---|---|
| 目的 | 转换接口,解决不兼容问题 | 简化接口,提供统一入口 |
| 接口数量 | 一对一转换 | 多个接口 → 一个简化接口 |
| 使用时机 | 集成已有系统/第三方库 | 简化复杂子系统的使用 |
| 典型例子 | Arrays.asList()、SLF4J |
Spring JdbcTemplate、Facade 门面 |
在 Spring / JDK 中的应用¶
| 框架/类 | 说明 |
|---|---|
Arrays.asList() |
将数组适配为 List 接口 |
Collections.enumeration() |
将 Collection 适配为 Enumeration |
Spring HandlerAdapter |
适配不同类型的 MVC Handler |
| SLF4J | 将不同日志框架适配为统一 API |
InputStreamReader |
将字节流适配为字符流 |
5. 边界:异常情况与常见误区¶
误区一:Arrays.asList() 返回的 List 不支持增删(运行期 UnsupportedOperationException)¶
// ❌ 错误:以为 Arrays.asList() 返回普通 ArrayList
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
list.add("d"); // 运行时抛出 UnsupportedOperationException!
// 原因:Arrays.asList() 返回的是 Arrays 内部的 ArrayList(适配器),
// 它是固定大小的,底层直接引用原数组,不支持 add/remove 操作
// 这是适配器模式的"接口不完全兼容"问题
// ✅ 正确:如果需要可变 List,用 new ArrayList<>() 包装
List<String> mutableList = new ArrayList<>(Arrays.asList(array));
mutableList.add("d"); // 正常工作
误区二:适配器中忘记处理异常转换(运行期问题)¶
// ❌ 错误:适配器直接抛出被适配者的异常,暴露了内部实现细节
public class StripeAdapter implements PaymentGateway {
@Override
public boolean charge(String userId, double amount, String currency) {
try {
return stripe.processPayment(new PaymentRequest(userId, amount, currency)).isSuccess();
} catch (StripeException e) {
throw e; // 直接抛出 Stripe 特有异常,调用方被迫依赖 Stripe SDK!
}
}
}
// ✅ 正确:将被适配者的异常转换为目标接口定义的异常
public class StripeAdapter implements PaymentGateway {
@Override
public boolean charge(String userId, double amount, String currency) {
try {
return stripe.processPayment(new PaymentRequest(userId, amount, currency)).isSuccess();
} catch (StripeException e) {
// 转换为系统内部异常,隐藏第三方实现细节
throw new PaymentException("支付处理失败: " + e.getMessage(), e);
}
}
}
误区三:过度使用适配器,掩盖了真正的设计问题(设计问题)¶
// ❌ 问题:系统内部接口设计混乱,到处用适配器"打补丁"
// 如果发现需要大量适配器,说明系统接口设计本身有问题
// 适配器应该用于集成外部系统,而不是修复内部设计缺陷
// ✅ 正确使用场景:
// 1. 集成第三方库/遗留系统
// 2. 统一多个相似但接口不同的实现
// 3. 不能修改源码的情况下复用已有类
6. 总结:面试标准化表达¶
高频面试题¶
Q1:适配器模式解决了什么问题?有哪两种实现方式?
适配器模式解决接口不兼容问题,让原本无法协同工作的类可以一起工作,常用于集成第三方库或遗留系统。有两种实现:①对象适配器(推荐),通过组合持有被适配者引用,灵活性高,可适配被适配者的子类;②类适配器,通过继承被适配者,Java 单继承限制使其不推荐使用。工作中最常见的是对象适配器,如将 Stripe、PayPal 等第三方支付 SDK 适配为系统内部的统一支付接口。
Q2:Arrays.asList() 为什么不能 add/remove?
Arrays.asList()是适配器模式的应用,它返回的是Arrays内部的私有ArrayList类(不是java.util.ArrayList),这个类继承自AbstractList,底层直接引用原数组,没有实现add/remove方法(调用会抛UnsupportedOperationException)。这是适配器"接口不完全兼容"的典型问题——适配器只转换了读取操作,没有支持修改操作。如需可变 List,应使用new ArrayList<>(Arrays.asList(array))。
Q3:Spring MVC 中的 HandlerAdapter 是什么设计模式?
Spring MVC 的
HandlerAdapter是适配器模式的经典应用。Spring MVC 支持多种 Handler 类型(注解@Controller、实现Controller接口、实现HttpRequestHandler接口等),DispatcherServlet通过HandlerAdapter统一调用这些不同类型的 Handler,而无需关心具体类型。每种 Handler 类型对应一个HandlerAdapter实现(如RequestMappingHandlerAdapter、SimpleControllerHandlerAdapter),这样新增 Handler 类型只需新增对应的 Adapter,不需要修改DispatcherServlet。
一句话记忆口诀:适配器转换接口,让不兼容的类协同工作,优先用组合(对象适配器)而非继承(类适配器),
Arrays.asList()和 SpringHandlerAdapter是最熟悉的例子。