适配器模式:让不兼容的接口握手言和
说实话,适配器模式是我在实际项目中用得最多的设计模式之一。为什么?因为现实世界里的代码,从来不会按照教科书那样完美设计。你总会遇到这样的情况:手里拿着一个三脚插头,墙上却只有两孔插座。怎么办?找个适配器呗。
在软件世界里,这个问题同样普遍。我接手过一个遗留系统,里面有个日志模块写得特别好,但它的接口是 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;
}
这段代码里,适配器做了三件事:
- 继承目标接口
TemperatureSensor - 持有被适配对象
OldFahrenheitSensor - 在接口方法中完成转换逻辑
注意:我曾经犯过一个错误——在适配器里直接 new 被适配对象。这会导致适配器与被适配者的生命周期耦合。正确的做法是:让调用方传入被适配对象的指针或引用,适配器只负责转换,不负责管理生命周期。
STL中的适配器示例
C++标准库本身就是适配器模式的最佳实践场。我挑几个典型的说说。
stack 适配器
std::stack 本质上就是一个适配器。它默认基于 deque 实现,但你可以换成 vector 或 list:
#include <stack>
#include <vector>
std::stack<int, std::vector<int>> myStack;
// stack 把 vector 的接口适配成了 LIFO 的栈接口
你看,stack 并没有重新实现数据结构,它只是把底层容器的接口包装成了 push、pop、top 这些栈操作。这就是适配器模式的精髓。
queue 适配器
同理,std::queue 默认基于 deque,提供了 FIFO 的队列接口:
#include <queue>
#include <list>
std::queue<int, std::list<int>> myQueue;
// queue 把 list 适配成了队列接口
函数适配器(C++17之前)
在 C++11/14 时代,std::bind 和 std::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 表达式,这些适配器用得少了,但设计思想依然值得学习。
知识体系总览
下面这张图总结了适配器模式的核心脉络:
避坑指南
最后,分享几个我踩过的坑:
- 不要过度适配:我曾经见过一个项目,为了统一接口,给每个类都套了一层适配器。结果代码变得又臭又长。记住,适配器是用来解决接口不兼容问题的,不是用来做架构设计的。
- 注意性能开销:对象适配器多了一层间接调用。在性能敏感的场景下,这个开销可能不可忽略。我遇到过音频处理程序,因为适配器导致延迟增加了 5 毫秒,最后不得不改用类适配器。
- 适配器不是万能药:如果两个系统的接口差异太大,适配器会让代码变得难以维护。这时候,重新设计接口可能是更好的选择。
适配器模式,说白了就是「让旧代码也能在新系统里发光发热」。它不炫技,但很实用。下次遇到接口不匹配的问题,先别急着改代码,想想能不能加个适配器。