适配器模式
在开发中,我们有时会遇到这样的情况:我们重写了系统的某个模块,但它的接口与其他模块的接口不匹配;或者我们想使用一个第三方库,但它提供的接口不能直接满足我们的需求。
这时候,修改历史遗留代码或第三方库的源代码,通常是不可行或不明智的,我们应该怎么做呢?
设计模式中的适配器模式(Adapter Pattern) 就扮演着类似的角色,它允许接口不兼容的对象之间能够相互合作。
看一个例子就能直观地理解适配器模式的用法了。假设我们有一个 Chinese
类,它有一个 speakChinese
方法只能输出中文:
// 被适配者 (Adaptee),只会说中文
class Chinese {
public void speakChinese() {
System.out.println("你好世界!");
}
}
但是我们的系统需要输出英文,怎么办呢?
很简单,请个翻译过来,让他帮忙把中文翻译成英文:
// 目标接口 (Target),期望输出英文
interface EnglishSpeaker {
void speakEnglish();
}
// 适配器 (Adapter),实现了目标接口,并持有一个被适配者的实例
class TranslatorAdapter implements EnglishSpeaker {
private Chinese adaptee;
// 通过构造函数传入被适配者对象
public TranslatorAdapter(Chinese adaptee) {
this.adaptee = adaptee;
}
@Override
public void speakEnglish() {
// 调用被适配者的方法
adaptee.speakChinese();
// 添加一些转换逻辑,满足目标接口的需求
System.out.println("Translated: Hello World!");
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
Chinese chinese = new Chinese();
EnglishSpeaker adapter = new TranslatorAdapter(chinese);
// 对于客户端来说,它只知道 EnglishSpeaker 接口,并调用 speakEnglish 方法
// 适配器正确地进行了接口转换,将中文翻译成了英文
adapter.speakEnglish();
// 你好世界!
// Translated: Hello World!
}
}
适配器模式主要包含以下几个核心角色:
- 目标(Target):客户端代码所期望的接口。在上面的例子中,就是
EnglishSpeaker
接口。 - 被适配者(Adaptee):需要被适配的类,它拥有客户端所需的功能,但接口与目标接口不兼容。在上面的例子中,就是
Chinese
类。 - 适配器(Adapter):连接目标和被适配者的桥梁。它实现了目标接口,并持有一个被适配者类的实例,将目标接口的调用转换为对被适配者接口的调用。在上面的例子中,就是
TranslatorAdapter
类。 - 客户端(Client):通过目标接口与适配器进行交互。
上面展示的是「对象适配器」,适配器通过构造函数传入被适配者对象,从而能够调用被适配者的方法。
还有一种「类适配器」,适配器继承被适配者类,从而能够调用被适配者的方法,类似这样:
class TranslatorAdapter extends Chinese implements EnglishSpeaker {
@Override
public void speakEnglish() {
// 调用被适配者的方法
super.speakChinese();
System.out.println("Translated: Hello World!");
}
}
可以看到,两种方法只是调用被适配者的方式不同,本质上是一样的。
由于现代软件开发推崇「组合优于继承」的设计理念,且像 Java 这样的语言不支持多重类继承,所以基于继承的「类适配器」不如基于组合的「对象适配器」常见。
更多场景
电源适配器
现实生活中,电源适配器就是适配器模式的经典例子。家用插座提供220V交流电,但手机、笔记本电脑等设备需要的是不同规格的直流电(如5V、9V、15V等)。电源适配器就是这个「翻译官」,它将220V交流电转换成设备所需的直流电。
让我们来模拟实现一个智能的电源适配器,能够根据设备的需求提供合适的电压。
首先定义电压的类型和数据结构:
// 枚举: 电压类型
public enum VoltageType {
AC, // 交流
DC // 直流
}
// 数据类: 封装电压值和类型
public record Voltage(int value, VoltageType type) {
@Override
public String toString() {
return String.format("%dV %s", value, type);
}
// 重写 equals 和 hashCode 以便在列表中正确比较
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Voltage voltage = (Voltage) o;
return value == voltage.value && type == voltage.type;
}
}
然后定义被适配者(家用插座):
// Adaptee: 被适配者 - 家用插座
public class HouseholdSocket {
public Voltage supply() {
System.out.println("家用插座: 提供 -> 220V AC");
return new Voltage(220, VoltageType.AC);
}
}
定义目标接口(智能充电协议):
// Target: 目标接口 - 智能充电协议
public interface SmartChargeTarget {
// 协商并提供电压
Voltage supplyVoltage(List<Voltage> deviceSupportedVoltages);
}
实现适配器(智能电源适配器):
// Adapter: 智能电源适配器
public class SmartPowerAdapter implements SmartChargeTarget {
private final List<Voltage> adapterSupportedVoltages;
private final HouseholdSocket adaptee;
// 构造函数,允许定义不同规格的适配器
public SmartPowerAdapter(HouseholdSocket adaptee, List<Voltage> supportedVoltages) {
this.adaptee = adaptee;
this.adapterSupportedVoltages = supportedVoltages;
System.out.println("-> 适配器已创建,支持输出: " + this.adapterSupportedVoltages);
}
@Override
public Voltage supplyVoltage(List<Voltage> deviceSupportedVoltages) {
Voltage bestMatch = findBestVoltageMatch(deviceSupportedVoltages);
if (bestMatch != null) {
System.out.println("适配器: 协商成功,匹配到最佳电压: " + bestMatch);
return convert(bestMatch);
} else {
System.out.println("适配器: 协商失败,没有找到双方都支持的电压模式。");
return null;
}
}
// 协商逻辑: 寻找设备和适配器都支持的最高电压
private Voltage findBestVoltageMatch(List<Voltage> deviceVoltages) {
for (Voltage deviceVoltage : deviceVoltages) {
if (adapterSupportedVoltages.contains(deviceVoltage)) {
return deviceVoltage;
}
}
return null;
}
private Voltage convert(Voltage dcOutput) {
Voltage acInput = adaptee.supply();
System.out.printf("适配器: 将 %s 转换为 %s\n", acInput, dcOutput);
return dcOutput;
}
}
客户端代码,各种电子设备 Device
通过适配器充电:
class Device {
private final String deviceName;
private final List<Voltage> supportedVoltages;
// 构造函数,允许定义不同型号的设备及其支持的电压
public Device(String deviceName, List<Voltage> supportedVoltages) {
this.deviceName = deviceName;
this.supportedVoltages = supportedVoltages;
}
public void startCharging(SmartChargeTarget charger) {
Voltage finalVoltage = charger.supplyVoltage(supportedVoltages);
if (finalVoltage != null) {
System.out.printf("%s: ✔ 当前充电电压为: %s%n", deviceName, finalVoltage);
} else {
System.out.printf("%s: ❌ 适配器不支持我的任何充电电压%n", deviceName);
}
}
}
public class Main {
public static void main(String[] args) {
// 220V 家用插座
HouseholdSocket socket = new HouseholdSocket();
// 创建一个适配器,支持 15V, 9V, 5V
SmartPowerAdapter powerfulAdapter = new SmartPowerAdapter(socket, List.of(
new Voltage(15, VoltageType.DC),
new Voltage(9, VoltageType.DC),
new Voltage(5, VoltageType.DC)
));
// 场景一: 现代手机使用全能适配器,协商到最佳电压
Device modernPhone = new Device("iPhone 17", List.of(
new Voltage(15, VoltageType.DC),
new Voltage(9, VoltageType.DC),
new Voltage(5, VoltageType.DC)
));
modernPhone.startCharging(powerfulAdapter);
// 适配器: 协商成功,匹配到最佳电压: 15V DC
// 家用插座: 提供 -> 220V AC
// 适配器: 将 220V AC 转换为 15V DC
// iPhone 17: ✔ 当前充电电压为: 15V DC
// 场景二: 老旧手机使用全能适配器,协商到兼容的低电压
Device oldPhone = new Device("Nokia", List.of(
// 只支持5V
new Voltage(5, VoltageType.DC)
));
oldPhone.startCharging(powerfulAdapter);
// 适配器: 协商成功,匹配到最佳电压: 5V DC
// 家用插座: 提供 -> 220V AC
// 适配器: 将 220V AC 转换为 5V DC
// Nokia: ✔ 充电协商成功!当前充电电压为: 5V DC
// 场景三: 无法适配,拒绝充电
// 创建一个只能输出5V的老旧适配器
SmartPowerAdapter oldAdapter = new SmartPowerAdapter(socket, List.of(
new Voltage(5, VoltageType.DC)
));
// 设备需要至少 15V 才能充电
Device gamingDevice = new Device("Gaming Device", List.of(
new Voltage(15, VoltageType.DC)
));
gamingDevice.startCharging(oldAdapter);
// 适配器: 协商失败,没有找到双方都支持的电压模式。
// Gaming Device: ❌ 充电协商失败,适配器不支持我的任何充电电压。
}
}
这个例子完美地展示了适配器模式的核心思想:
- 目标(Target):
SmartChargeTarget
接口,定义了智能充电的协商协议。 - 被适配者(Adaptee):
HouseholdSocket
类,只能提供 220V 交流电。 - 适配器(Adapter):
SmartPowerAdapter
类,将 220V 交流电转换为设备需要的直流电。 - 客户端(Client):各种电子设备
Device
通过适配器获得电力供应。
通过适配器,不同电压需求的设备都可以从同一个家用插座获得合适的电力供应。
Java I/O
Java 的 I/O 类库充满了适配器模式的身影。InputStream
是字节流,而 Reader
是字符流。如果我们想按字符读取一个基于字节流的输入源(比如 FileInputStream
),就需要一个适配器。
InputStreamReader
就是这个适配器,它接收一个 InputStream
对象(被适配者),并实现了 Reader
接口(目标),从而将字节流适配为字符流。
// FileInputStream 读取 01 字节流 (Adaptee)
FileInputStream fis = new FileInputStream("test.txt");
// InputStreamReader 是一个适配器,将 01 字节流转换为 UTF-8 字符流 (Target)
Reader reader = new InputStreamReader(fis, StandardCharsets.UTF_8);
// 现在我们可以用 Reader 的 API 来按字符读取文件
int character;
while ((character = reader.read()) != -1) {
System.out.print((char) character);
}
reader.close();
在这个例子中:
- 目标(Target):
Reader
接口,期望按字符读取文件。 - 被适配者(Adaptee):
FileInputStream
类,读取字节流。 - 适配器(Adapter):
InputStreamReader
类,将字节流转换为字符流。
总结
适配器模式的主要优势:
- 增强代码的灵活性和可扩展性:客户端代码与具体的被适配者解耦,更换被适配者对客户端是透明的。
- 符合单一职责原则:将接口转换的逻辑封装在适配器中,而不是散落在业务代码各处。
适配器模式的主要缺点:
增加了代码的复杂性:需要额外编写适配器类和目标接口。如果可以的话,直接修改被适配者的接口会更简单。
总的来说,当你需要集成一个接口不兼容的现有组件时,适配器模式是一个非常强大且实用的工具。