在开发中,我们有时会遇到这样的情况:我们重写了系统的某个模块,但它的接口与其他模块的接口不匹配;或者我们想使用一个第三方库,但它提供的接口不能直接满足我们的需求。
这时候,修改历史遗留代码或第三方库的源代码,通常是不可行或不明智的,我们应该怎么做呢?
设计模式中的适配器模式(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();
}
}适配器模式主要包含以下几个核心角色:
- 目标(Target):客户端代码所期望的接口。在上面的例子中,就是
EnglishSpeaker接口。 - 被适配者(Adaptee):需要被适配的类,它拥有客户端所需的功能,但接口与目标接口不兼容。在上面的例子中,就是
Chinese类。 - 适配器(Adapter):连接目标和被适配者的桥梁。它实现了目标接口,并持有一个被适配者类的实例,将目标接口的调用转换为对被适配者接口的调用。在上面的例子中,就是
TranslatorAdapter类。 - 客户端(Client):通过目标接口与适配器进行交互。
上面展示的是「对象适配器」,适配器通过构造函数传入被适配者对象,从而能够调用被适配者的方法。
还有一种「类适配器」,适配器继承被适配者类,从而能够调用被适配者的方法,类似这样:
关于代码示例
类适配器的核心是「适配器同时继承被适配者并实现目标接口」,本质依赖语言的类继承特性。Java、C++、Python、TypeScript 都能直观表达这一机制;Go 没有类继承(其 struct embedding 在语义上更接近组合,无法体现类适配器与对象适配器在「继承 vs 组合」上的对比),因此本块代码示例不包含 Go 实现。
// 类适配器:继承被适配者并实现目标接口
class TranslatorAdapter extends Chinese implements EnglishSpeaker {
@Override
public void speakEnglish() {
// 调用被适配者的方法(来自父类)
super.speakChinese();
System.out.println("Translated: Hello World!");
}
}可以看到,两种方法只是调用被适配者的方式不同,本质上是一样的。
由于现代软件开发推崇「组合优于继承」的设计理念,且像 Java 这样的语言不支持多重类继承,所以基于继承的「类适配器」不如基于组合的「对象适配器」常见。