适配器模式

在开发中,我们有时会遇到这样的情况:我们重写了系统的某个模块,但它的接口与其他模块的接口不匹配;或者我们想使用一个第三方库,但它提供的接口不能直接满足我们的需求。

这时候,修改历史遗留代码或第三方库的源代码,通常是不可行或不明智的,我们应该怎么做呢?

设计模式中的适配器模式(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 这样的语言不支持多重类继承,所以基于继承的「类适配器」不如基于组合的「对象适配器」常见。