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

说实话,适配器模式是我在实际项目中用得最多的设计模式之一。为什么?因为现实世界里的代码,从来不会按照教科书那样完美设计。你总会遇到这样的情况:手里拿着一个三脚插头,墙上却只有两孔插座。怎么办?找个适配器呗。

在软件世界里,这个问题同样普遍。我接手过一个遗留系统,里面有个日志模块写得特别好,但它的接口是 void log(string msg)。新系统要求的是 void write(LogEntry entry)。改老代码?风险太大。重写新模块?浪费时间。这时候,适配器模式就是你的救星。

模式动机与定义

适配器模式的核心思想很简单:将一个类的接口转换成客户期望的另一个接口。它让原本因接口不匹配而无法一起工作的类,能够协同工作。

你想想看,我们为什么要用这个模式?说白了就三个场景:

  • 复用现有类:老代码功能没问题,只是接口对不上
  • 统一接口:多个第三方库各有各的调用方式,你想统一成一套
  • 解耦:不想让高层代码直接依赖底层实现细节

关键定义:适配器模式属于结构型模式,它通过组合或继承的方式,让原本不兼容的接口协同工作。适配器在客户端和目标类之间充当中间人的角色。

类适配器 vs 对象适配器

这里有个经典问题:用继承还是用组合?两种方式各有千秋,我分别说说。

类适配器(继承方式)

类适配器通过多重继承来实现。C++支持多继承,所以这条路走得通。它的优点是:

  • 可以重写被适配类的方法
  • 代码更直接,不需要额外对象

但缺点也很明显:

  • 需要多重继承,增加了耦合
  • 不能适配被适配类的子类

对象适配器(组合方式)

对象适配器通过持有被适配对象的引用来工作。我个人更偏爱这种方式,原因有三:

  • 低耦合:适配器只依赖接口,不依赖具体类
  • 灵活性高:可以适配任何实现了目标接口的类
  • 符合组合优于继承的原则

我的建议:除非你有特别强的理由需要重写被适配类的方法,否则优先选择对象适配器。我在项目中几乎都用对象适配器,只有一次因为性能敏感(每秒百万级调用)才用了类适配器。

C++代码实现

咱们直接看代码。假设我们有一个老式的温度传感器,返回华氏温度。新系统需要摄氏温度。

// 目标接口:新系统期望的接口
class TemperatureSensor {
public:
    virtual ~TemperatureSensor() = default;
    virtual double getTemperatureCelsius() = 0;
};

// 被适配者:老式传感器,只返回华氏温度
class OldFahrenheitSensor {
public:
    double getTemperatureFahrenheit() {
        return 98.6;  // 模拟数据
    }
};

// 对象适配器
class TemperatureAdapter : public TemperatureSensor {
private:
    OldFahrenheitSensor* oldSensor;  // 持有被适配对象的引用

public:
    TemperatureAdapter(OldFahrenheitSensor* sensor) : oldSensor(sensor) {}

    double getTemperatureCelsius() override {
        double fahrenheit = oldSensor->getTemperatureFahrenheit();
        return (fahrenheit - 32.0) * 5.0 / 9.0;  // 转换逻辑
    }
};

// 客户端代码
int main() {
    OldFahrenheitSensor oldSensor;
    TemperatureAdapter adapter(&oldSensor);

    double celsius = adapter.getTemperatureCelsius();
    // 输出:37.0°C
    return 0;
}

这段代码里,适配器做了三件事:

  1. 继承目标接口 TemperatureSensor
  2. 持有被适配对象 OldFahrenheitSensor
  3. 在接口方法中完成转换逻辑

注意:我曾经犯过一个错误——在适配器里直接 new 被适配对象。这会导致适配器与被适配者的生命周期耦合。正确的做法是:让调用方传入被适配对象的指针或引用,适配器只负责转换,不负责管理生命周期。

STL中的适配器示例

C++标准库本身就是适配器模式的最佳实践场。我挑几个典型的说说。

stack 适配器

std::stack 本质上就是一个适配器。它默认基于 deque 实现,但你可以换成 vectorlist

#include <stack>
#include <vector>

std::stack<int, std::vector<int>> myStack;
// stack 把 vector 的接口适配成了 LIFO 的栈接口

你看,stack 并没有重新实现数据结构,它只是把底层容器的接口包装成了 pushpoptop 这些栈操作。这就是适配器模式的精髓。

queue 适配器

同理,std::queue 默认基于 deque,提供了 FIFO 的队列接口:

#include <queue>
#include <list>

std::queue<int, std::list<int>> myQueue;
// queue 把 list 适配成了队列接口

函数适配器(C++17之前)

在 C++11/14 时代,std::bindstd::mem_fn 都是典型的函数适配器。它们把不同签名的函数适配成统一的调用形式:

#include <functional>

void func(int a, int b) {
    // ...
}

auto adapted = std::bind(func, 10, std::placeholders::_1);
adapted(20);  // 等价于 func(10, 20)

虽然 C++17 之后有了 lambda 表达式,这些适配器用得少了,但设计思想依然值得学习。

知识体系总览

下面这张图总结了适配器模式的核心脉络:

适配器模式知识体系 模式动机 • 复用现有类 • 统一接口 • 解耦高层与底层 两种实现 • 类适配器(继承) • 对象适配器(组合) • 推荐:对象适配器 STL示例 • stack 适配 deque • queue 适配 list • 函数适配器 bind 核心思想 将一个类的接口转换成客户期望的另一个接口 让原本因接口不匹配而无法一起工作的类能够协同工作 适配器模式 · 结构型模式

避坑指南

最后,分享几个我踩过的坑:

  • 不要过度适配:我曾经见过一个项目,为了统一接口,给每个类都套了一层适配器。结果代码变得又臭又长。记住,适配器是用来解决接口不兼容问题的,不是用来做架构设计的。
  • 注意性能开销:对象适配器多了一层间接调用。在性能敏感的场景下,这个开销可能不可忽略。我遇到过音频处理程序,因为适配器导致延迟增加了 5 毫秒,最后不得不改用类适配器。
  • 适配器不是万能药:如果两个系统的接口差异太大,适配器会让代码变得难以维护。这时候,重新设计接口可能是更好的选择。

适配器模式,说白了就是「让旧代码也能在新系统里发光发热」。它不炫技,但很实用。下次遇到接口不匹配的问题,先别急着改代码,想想能不能加个适配器。