适配器模式:让不兼容的接口“握手言和”
说实话,适配器模式是我在实际项目中用得最多的设计模式之一。为什么?因为现实世界里的代码,从来不会按照你理想中的剧本走。你接手一个老系统,里面跑着十年前写的接口;新来的第三方库,又只认最新的协议。两边都想用,但就是接不上——这时候,适配器就是那个“翻译官”。
模式动机:为什么需要适配器?
先讲个我自己的经历。几年前我参与一个嵌入式项目,底层通信模块用的是串口协议,上层业务逻辑却要求走 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,适配器在内部做了转换。这就是适配器的魔力——让新旧代码和平共处。
实际应用场景
适配器模式的应用场景太多了,我挑几个典型的说说。
- 旧系统接口迁移:老系统暴露的接口与新系统不匹配,但又不能直接修改老系统。加一层适配器,平滑过渡。
- 第三方库封装:不同厂商的 SDK 接口各异,用适配器统一成公司内部的接口标准。我在做物联网平台时,就曾用适配器统一了 5 家不同厂商的传感器协议。
- 数据格式转换:比如 XML 转 JSON,或者不同数据库驱动的接口适配。适配器在这里充当“数据翻译官”。
- 测试桩(Test Stub):在单元测试中,用适配器模拟外部依赖,让测试代码不依赖真实环境。
知识体系结构图
下面我用一张 SVG 图来总结适配器模式的核心逻辑。这张图展示了适配器如何连接客户端和适配者。
从这张图可以看得很清楚:客户端只跟目标接口打交道,适配器在中间做“翻译”。适配者完全不知道适配器的存在——这就是“对象适配器”的典型结构。
总结
适配器模式不是什么高深的技术,但它解决了一个非常实际的问题:让不兼容的接口能够协作。我个人习惯在项目初期就预留适配层,这样后期接入第三方库或迁移旧系统时,会从容很多。
记住两个关键点:
- 优先使用对象适配器(组合),而不是类适配器(继承)
- 适配器是“胶水”,不是“万能药”。如果接口设计本身有问题,该重构就重构
嗯,适配器模式就讲到这里。代码不多,但背后的思想值得反复琢磨。下次你遇到接口不匹配的问题时,不妨想想——是不是该请个“翻译官”了?