什么是适配器模式?
核心思想:
适配器模式是一种结构型设计模式,它允许不兼容的接口能够协同工作,它就像一个电源适配器(充电头)一样,将一个类的接口转换成客户端期望的另一个接口,从而使原本因接口不兼容而无法一起工作的类可以一起工作。
生活中的例子:
- 电源适配器:中国的插座是两孔或三孔的扁型,而很多国外电子设备的插头是两圆脚的,电源适配器(充电头)就是一个中间层,它将外国的插头转换成中国插座可以接受的形状。
- 读卡器:你的电脑没有 SD 卡槽,但你有一个读卡器,读卡器将 SD卡的接口转换成了 USB 接口,这样你的电脑(客户端)就能通过 USB 接口读取 SD 卡中的数据了。
UML 类图:
+----------------+ +-----------------------+ +----------------+
| Client | | Adapter | | Adaptee |
+----------------+ +-----------------------+ +----------------+
| +operation() |------>| +adaptee: Adaptee |------>| +specificRequest() |
+----------------+ | | +----------------+
| +request() |
+-----------------------+
图解:
- Client (客户端):需要调用
request()方法的代码。 - Target (目标接口):客户端期望的接口,这里是
request()方法。 - Adaptee (被适配者):已经存在的、具有特定接口的类,它的方法是
specificRequest(),与客户端期望的接口不兼容。 - Adapter (适配器):持有
Adaptee的引用,并实现了Target接口,在request()方法内部,它会调用Adaptee的specificRequest()方法,从而完成转换。
适配器模式的两种主要实现方式
适配器模式分为两种:类适配器 和 对象适配器,在 Java 中,由于不支持多重继承(一个类不能同时继承两个类),所以类适配器使用较少,对象适配器是更常用和推荐的方式。
对象适配器(推荐)
这是最常用、最灵活的方式,它通过组合来实现,即适配器类持有一个被适配者的实例。
实现步骤:
- 定义一个 目标接口,这是客户端期望的接口。
- 定义一个 被适配者类,这是已存在的、具有特定接口的类。
- 创建一个 适配器类,实现目标接口,并在内部持有一个被适配者类的实例。
- 在适配器类的目标方法中,调用被适配者类的方法。
代码示例:对象适配器
假设我们要开发一个日志系统,客户端希望统一使用 LogApi 接口来记录日志,但项目中已经有一个功能强大的第三方日志库 LegacyLogger,它的接口与我们期望的不同。
第1步:定义目标接口
这是客户端希望使用的标准接口。
// 目标接口:客户端期望的日志API
public interface LogApi {
void log(String message);
}
第2步:定义被适配者类
这是已经存在的、功能完善但接口不兼容的类。
// 被适配者:已有的第三方日志库,它的接口我们不希望直接修改
public class LegacyLogger {
// 它的日志方法叫 doLog,参数和我们的期望不同
public void doLog(String level, String message) {
// 模拟一个复杂的日志记录过程
System.out.println("[Legacy Logger] [" + level.toUpperCase() + "] " + message);
}
}
第3步:创建适配器类
这是适配器的核心,它连接了 LogApi 和 LegacyLogger。
// 适配器:实现了目标接口,并组合了被适配者
public class LoggerAdapter implements LogApi {
// 持有被适配者的实例
private LegacyLogger legacyLogger;
// 通过构造函数注入被适配者
public LoggerAdapter(LegacyLogger legacyLogger) {
this.legacyLogger = legacyLogger;
}
@Override
public void log(String message) {
// 将客户端的简单调用,转换成被适配者复杂调用的形式
// 假设我们默认使用 "INFO" 级别
this.legacyLogger.doLog("INFO", message);
}
}
第4步:客户端使用
客户端代码现在可以完全基于 LogApi 接口编程,无需关心 LegacyLogger 的存在。
public class Client {
public static void main(String[] args) {
// 1. 客户端期望使用 LogApi
LogApi logger = new LoggerAdapter(new LegacyLogger());
// 2. 客户端可以像调用标准接口一样调用 log 方法
logger.log("This is a log message from the client.");
// 3. 输出结果:
// [Legacy Logger] [INFO] This is a log message from the client.
}
}
总结对象适配器:
客户端通过 LogApi 接口与 LoggerAdapter 交互。LoggerAdapter 在内部将 log() 调用翻译成 LegacyLogger 的 doLog() 调用,完美实现了接口的转换。
类适配器(不常用,了解即可)
类适配器通过继承来实现,适配器类同时继承被适配者类和实现目标接口。
UML 类图:
+----------------+ +-----------------------+
| Client | | Adapter |
+----------------+ +-----------------------+
| +operation() |------>| +adaptee: Adaptee |
+----------------+ | |
| +request() |
+-----------------------+
^
|
+-----------------------+
| Adaptee |
+-----------------------+
| +specificRequest() |
+-----------------------+
Java 代码示例(仅作演示):
// 目标接口
interface LogApi {
void log(String message);
}
// 被适配者类
class LegacyLogger {
public void doLog(String level, String message) {
System.out.println("[Legacy Logger] [" + level.toUpperCase() + "] " + message);
}
}
// 适配器类:同时继承 LegacyLogger 并实现 LogApi
// 注意:Java 只能单继承,所以这种方式灵活性较差
class ClassLoggerAdapter extends LegacyLogger implements LogApi {
@Override
public void log(String message) {
// 直接调用父类的方法
this.doLog("INFO", message);
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
LogApi logger = new ClassLoggerAdapter();
logger.log("This is a log message from the client.");
}
}
为什么对象适配器更推荐?
- 灵活性:对象适配器通过组合,可以在运行时动态地指定被适配者对象,而类适配器在编译时就确定了被适配者(通过继承)。
- 解耦:适配器与被适配者是松耦合的(通过组合关系),而不是紧耦合的(通过继承关系)。
- 多重继承的限制:Java 不支持多重继承,如果被适配者本身已经继承了一个类,那么类适配器就无法再继承它了。
适配器模式的优缺点
优点
- 提高复用性:使得原本由于接口不兼容而无法一起工作的类可以协同工作,复用了现有代码。
- 增加灵活性:引入适配器后,系统就不需要修改原有代码,符合“开闭原则”(对扩展开放,对修改关闭)。
- 解耦:将客户端与被适配者解耦,客户端只与目标接口交互。
缺点
- 增加复杂性:系统中会增加新的类和接口,使得系统整体结构变得复杂。
- 过度使用:如果被适配者类本身就提供了需要的功能,或者可以通过简单重构来匹配接口,那么使用适配器模式就是不必要的,会增加不必要的间接层。
适配器模式的应用场景
适配器模式在以下场景中特别有用:
- 需要整合第三方库或遗留系统:这是最经典的应用场景,当你想使用一个功能强大的旧系统或第三方库,但它的接口与你的系统不兼容时,适配器模式是最佳选择。
- 统一多个类的接口:当系统需要使用多个不同的类,但这些类的接口又各不相同时,可以创建适配器,为它们提供一个统一的接口,方便客户端调用。
- 版本升级:当系统升级某个模块时,新模块的接口可能与旧模块不兼容,可以使用适配器来保持与调用方代码的兼容性,实现平滑过渡。
与其他模式的区别
-
适配器模式 vs 代理模式:
- 目的不同:适配器模式的目的是转换接口,让两个不兼容的接口能够协同工作,代理模式的目的是控制对对象的访问,通常在不改变接口的前提下,增加一些额外的功能(如日志、权限检查)。
- 关注点不同:适配器关注“转换”,代理关注“控制”。
-
适配器模式 vs 装饰器模式:
- 目的不同:装饰器模式的目的是动态地给对象添加新的职责,而不改变其接口,适配器模式的目的是改变接口。
- 结构不同:装饰器通常遵循“组合+继承”的结构,并且装饰的对象和被装饰的对象通常属于同一个类型体系,适配器则不一定。
| 特性 | 描述 |
|---|---|
| 模式类型 | 结构型模式 |
| 核心意图 | 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 |
| 关键角色 | Target (目标接口), Adaptee (被适配者), Adapter (适配器) |
| 主要实现 | 对象适配器(组合) 是 Java 中最常用和推荐的方式。 |
| 优点 | 提高代码复用性,增加系统灵活性,符合开闭原则。 |
| 缺点 | 增加系统复杂度,可能产生不必要的间接层。 |
| 应用场景 | 整合第三方库、遗留系统,统一多个类的接口。 |
掌握适配器模式,能让你在处理复杂系统整合和接口兼容性问题时游刃有余。
