适配器模式:让不兼容的接口“握手言和”

说实话,适配器模式是我在实际项目中用得最多的设计模式之一。为什么?因为现实世界里的代码,从来不会按照你理想中的剧本走。你接手一个老系统,里面跑着十年前写的接口;新来的第三方库,又只认最新的协议。两边都想用,但就是接不上——这时候,适配器就是那个“翻译官”。

模式动机:为什么需要适配器?

先讲个我自己的经历。几年前我参与一个嵌入式项目,底层通信模块用的是串口协议,上层业务逻辑却要求走 TCP/IP 封装。改底层?成本太高,而且稳定性已经验证过了。改上层?业务逻辑耦合了太多东西,一动就崩。怎么办?

我当时就在中间加了一层——一个适配器。它把串口的数据“包装”成 TCP/IP 的样子,上层调用时完全感觉不到底层是串口。这就是适配器模式的核心动机:让原本因接口不匹配而无法一起工作的类,能够协同工作

说白了,适配器就是做“接口转换”的。你想想看,生活中我们用的电源转换器、USB 转 Type-C 的转接头,都是这个道理。代码世界也一样——你有一个目标接口(Target),一个已有的适配者(Adaptee),但它们的接口对不上。适配器(Adapter)夹在中间,把 Adaptee 的接口转换成 Target 期望的样子。

核心动机: 解决接口不兼容问题,复用现有类,而不修改其源代码。

模式定义:适配器模式是什么?

适配器模式(Adapter Pattern)属于结构型设计模式。它的定义很简洁:将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

嗯,这里要注意一点:适配器模式有两种实现方式——类适配器对象适配器。两者各有优劣,我后面会详细讲。先记住一个原则:能用对象适配器,就别用类适配器。为什么?因为组合优于继承,这是我在项目里踩过坑之后才深刻理解的。

类适配器 vs 对象适配器

这两种方式,说白了就是“继承”和“组合”的选择题。

类适配器(通过继承实现)

类适配器使用多重继承(在 C++ 中)来实现。Adapter 同时继承 Target 和 Adaptee,把 Adaptee 的接口“覆盖”成 Target 的样子。

我曾经在一个小型工具库中用过类适配器,当时觉得代码很简洁。但后来发现,这种方式的灵活性很差——因为继承是静态的,你没法在运行时切换适配的对象。

优点 缺点
代码量少,实现简单 需要多重继承,增加耦合
可以重写 Adaptee 的部分行为 无法适配 Adaptee 的子类
编译时确定类型,性能略高 灵活性差,运行时无法切换

对象适配器(通过组合实现)

对象适配器是更推荐的方式。Adapter 持有 Adaptee 的指针或引用,在内部调用 Adaptee 的方法,但对外暴露 Target 的接口。

我在实际项目中几乎都用对象适配器。为什么?因为组合比继承灵活得多。你可以随时换掉 Adaptee 对象,甚至可以在适配器中加入缓存、日志、校验等额外逻辑。这些在类适配器里很难做到。

优点 缺点
灵活性高,运行时可以切换 代码量稍多
可以适配 Adaptee 及其所有子类 需要额外管理对象生命周期
符合“组合优于继承”原则 间接调用,性能略有损耗

C++ 代码实现

下面我给出一个完整的对象适配器示例。场景是这样的:我们有一个老式的 LegacyPrinter,它只能打印字符串。但新系统要求使用 ModernPrinterInterface,需要支持打印文档对象。适配器就派上用场了。

// 目标接口:新系统期望的打印机接口
class ModernPrinterInterface {
public:
    virtual ~ModernPrinterInterface() = default;
    virtual void printDocument(const std::string& doc) = 0;
};

// 适配者:老式打印机,只能打印字符串
class LegacyPrinter {
public:
    void printText(const std::string& text) {
        std::cout << "[LegacyPrinter] " << text << std::endl;
    }
};

// 对象适配器:把 LegacyPrinter 包装成 ModernPrinterInterface
class PrinterAdapter : public ModernPrinterInterface {
private:
    LegacyPrinter* legacyPrinter_;  // 持有适配者对象

public:
    explicit PrinterAdapter(LegacyPrinter* printer) 
        : legacyPrinter_(printer) {}

    void printDocument(const std::string& doc) override {
        // 转换逻辑:把文档格式转成老式打印机可识别的文本
        std::string converted = "[Adapted] " + doc;
        legacyPrinter_->printText(converted);
    }
};

// 客户端代码
int main() {
    LegacyPrinter oldPrinter;
    PrinterAdapter adapter(&oldPrinter);

    // 客户端只认识 ModernPrinterInterface
    ModernPrinterInterface* printer = &adapter;
    printer->printDocument("Hello, Adapter Pattern!");

    return 0;
}

你看,客户端完全不知道底层跑的是老式打印机。它只调用 printDocument,适配器在内部做了转换。这就是适配器的魔力——让新旧代码和平共处

避坑指南: 我曾经在适配器中忘记处理 Adaptee 的生命周期,导致野指针崩溃。记住:如果适配器不拥有 Adaptee 的所有权,一定要确保 Adaptee 的生命周期长于适配器。或者使用 shared_ptr 管理。

实际应用场景

适配器模式的应用场景太多了,我挑几个典型的说说。

  • 旧系统接口迁移:老系统暴露的接口与新系统不匹配,但又不能直接修改老系统。加一层适配器,平滑过渡。
  • 第三方库封装:不同厂商的 SDK 接口各异,用适配器统一成公司内部的接口标准。我在做物联网平台时,就曾用适配器统一了 5 家不同厂商的传感器协议。
  • 数据格式转换:比如 XML 转 JSON,或者不同数据库驱动的接口适配。适配器在这里充当“数据翻译官”。
  • 测试桩(Test Stub):在单元测试中,用适配器模拟外部依赖,让测试代码不依赖真实环境。
注意: 不要滥用适配器。如果接口不匹配是因为设计本身有问题,应该重构而不是加适配器。适配器是“胶水代码”,用多了会让系统变得混乱。

知识体系结构图

下面我用一张 SVG 图来总结适配器模式的核心逻辑。这张图展示了适配器如何连接客户端和适配者。

适配器模式核心结构 客户端 目标接口 (Target) 适配器 (Adapter) 适配者 (Adaptee) 调用 实现 持有/调用 工作流程 1. 客户端调用目标接口的方法 2. 适配器将请求转发给适配者 3. 适配者执行实际逻辑,结果返回给客户端

从这张图可以看得很清楚:客户端只跟目标接口打交道,适配器在中间做“翻译”。适配者完全不知道适配器的存在——这就是“对象适配器”的典型结构。

总结

适配器模式不是什么高深的技术,但它解决了一个非常实际的问题:让不兼容的接口能够协作。我个人习惯在项目初期就预留适配层,这样后期接入第三方库或迁移旧系统时,会从容很多。

记住两个关键点:

  • 优先使用对象适配器(组合),而不是类适配器(继承)
  • 适配器是“胶水”,不是“万能药”。如果接口设计本身有问题,该重构就重构

嗯,适配器模式就讲到这里。代码不多,但背后的思想值得反复琢磨。下次你遇到接口不匹配的问题时,不妨想想——是不是该请个“翻译官”了?


公众号:蓝海资料掘金营,微信deep3321