系列文章:单例模式:【C++/Qt 进阶】设计模式系列(1):单例模式——饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客工厂模式与抽象工厂模式:【C++/Qt 进阶】设计模式系列(2):工厂模式——从游戏角色到披萨店-CSDN博客建造者模式:【C++/Qt 进阶】设计模式系列(3): 建造者模式——把“复杂对象的组装“从业务代码里剥离出来-CSDN博客原型模式:【C++/Qt 进阶】设计模式系列(4):原型模式——用“克隆“代替“重新构造“-CSDN博客适配器模式(本篇)桥接模式(持续更新中)写在前面⚠️ 注意:前四篇(单例 / 工厂 / 建造者 / 原型)均为创建型模式;从本篇起正式进入结构型模式的第一站——适配器。上一讲我们通过原型模式解决了“复制当前对象”的问题。但在真实的 Qt 项目中,更常见的挑战是:“我手头有一个现成的类(可能是第三方库,也可能是遗留代码),它的接口与系统需求不匹配,且无法修改源码。该怎么办?”举个典型的 Qt 工程师都遇到过的场景——日志系统迁移:// 旧代码:全量使用 qDebug() void SomeService::doWork() { qDebug() << "step 1"; // 输出到 stderr // ... 大量业务逻辑 ... qDebug() << "done"; }如今需要替换为企业级日志库(如 spdlog、QsLog,或自研 logger)。但项目中有 200 多个.cpp文件都直接调用qDebug(),逐个修改?恐怕要改到明年。另一个常见场景——第三方 SDK 接口风格与项目规范不符:// 第三方相机接口(错误码风格) class VendorCamera { public: int Open(const char* dev); int Capture(unsigned char* buf, int* size); void Close(); }; // 项目统一接口(Qt 风格) class ICamera { public: virtual ~ICamera() = default; virtual bool open() = 0; virtual QImage grab() = 0; virtual void close() = 0; };若直接使用VendorCamera,所有调用方都得重复编写if (rc != 0) handle_error(...)的样板代码。适配器模式正是为此类“接口不兼容”问题提供一层“翻译层”。记忆口诀:“两头不对,中间一塞”——目标接口、适配者、客户端各司其职,适配器作为中间翻译者,打通沟通壁垒。一、30 秒看懂适配器模式维度工厂模式(造新车)适配器模式(贴翻译膜)解决什么创建对象接口转换关注重点选哪种类型让“不兼容”的类能协作适用时机字段少、构造简单接入老系统 / 第三方 SDK典型应用产品族 / 套件日志封装 / 协议转换 / SDK 包装一句话总结:不修改现有类,让它能被新系统“翻译”着使用。在 C++ 中,通常采用对象适配(组合被适配者指针),少数情况下使用类适配(公有继承被适配者并实现目标接口)。二、UML 类图与角色以下是基于纯 C++/Qt 视角的适配器模式结构图,采用 Qt 容器、std::unique_ptr与虚函数声明的典型写法。┌──────────────────────────────┐ │ class ICamera │ ← 目标接口(Target) │ { │ 客户端期望的统一接口 │ public: │ │ virtual ~ICamera() = default; │ virtual bool open() │ │ = 0; │ │ virtual QImage grab() │ │ = 0; │ │ virtual void close() = 0; │ │ }; │ └───────────────┬──────────────┘ │ implements / 继承 + 实现 ▼ ┌──────────────────────────────┐ │ class VendorCameraAdapter : │ ← 适配器(Adapter) │ public ICamera │ │ { │ │ public: │ │ explicit VendorCameraAdapter( │ VendorCamera* src) │ 构造时注入被适配者 │ : m_src(src) {} │ │ bool open() override; │ 翻译:错误码 → bool │ QImage grab() override; │ 翻译:原始数据 → QImage │ void close() override; │ 翻译:返回值 → void │ private: │ │ std::unique_ptr<VendorCamera> m_src; │ ← 持有被适配者(组合) │ }; │ └───────────────┬──────────────┘ │ 持有 ▼ ┌──────────────────────────────┐ │ class VendorCamera │ ← 适配者(Adaptee) │ { │ 已有或第三方类,接口不兼容 │ public: │ │ int Open(const char* dev); │ │ int Capture(unsigned char*, int* size); │ void Close(); │ │ }; │ └──────────────────────────────┘ ▲ │ 调用 ┌───────────────┴──────────────┐ │ class CameraClient │ ← 客户端(Client) │ { │ 只依赖 ICamera 接口 │ void shoot(ICamera* cam); │ │ }; │ └──────────────────────────────┘三个核心角色角色职责C++/Qt 中的体现目标接口(Target)客户端期望的统一接口class ICamera { virtual open()/grab()/close() = 0; }适配者(Adaptee)已存在但接口不兼容的类(第三方或旧代码)VendorCamera(int Open()风格)适配器(Adapter)实现目标接口,内部组合适配者,完成接口翻译VendorCameraAdapter💡C++ 特性提示:相比 Java 的extends继承方式,C++ 更倾向于使用组合而非继承来实现适配器,以避免多重继承带来的复杂性和耦合风险。因此,对象适配是主流选择。三、基础代码实现(C++/Qt 版)以下通过一个 Qt 工程师最熟悉的“统一日志门面”示例,将qDebug()、std::cout与假想的RemoteLogger三种日志实现统一为一个ILogger接口。3.1 目标接口// ilogger.h #include <QString> class ILogger { public: virtual ~ILogger() = default; virtual void info(const QString& msg) = 0; virtual void warn(const QString& msg) = 0; virtual void error(const QString& msg) = 0; };3.2 适配者 1:Qt 内置qDebug()// qdebuglogger.h #include <QDebug> #include "ilogger.h" // 适配者:Qt 风格,无类仅函数 // 使用空类包装,使其与其他适配者对齐 class QtDebugBackend { public: void output(QtMsgType level, const QString& msg) { qDebug().noquote() << msg; } };3.3 适配者 2:远程日志服务// remotelogger.h #include <QString> #include <QNetworkAccessManager> // 适配者:第三方团队提供的 HTTP 日志 SDK class RemoteLogger { public: int sendLog(const char* level, const char* msg) { return postHttp(level, msg); } private: int postHttp(const char*, const char*); };3.4 适配器:统一前两者至ILogger// remote_logger_adapter.h #include "ilogger.h" #include "remotelogger.h" #include <memory> // 对象适配:组合式封装 RemoteLogger class RemoteLoggerAdapter : public ILogger { public: explicit RemoteLoggerAdapter(std::unique_ptr<RemoteLogger> logger) : m_logger(std::move(logger)) {} void info(const QString& msg) override { if (m_logger->sendLog("INFO", msg.toUtf8().constData()) != 0) { // 失败兜底:降级到本地输出 qWarning() << "[fallback]" << msg; } } void warn(const QString& msg) override { m_logger->sendLog("WARN", msg.toUtf8().constData()); } void error(const QString& msg) override { m_logger->sendLog("ERROR", msg.toUtf8().constData()); } private: std::unique_ptr<RemoteLogger> m_logger; }; // 另一适配器:将 QtDebugBackend 包装为 ILogger class QtDebugAdapter : public ILogger { public: void info(const QString& msg) override { m_backend.output(QtInfoMsg, msg); } void warn(const QString& msg) override { m_backend.output(QtWarningMsg, msg); } void error(const QString& msg) override { m_backend.output(QtCriticalMsg, msg); } private: QtDebugBackend m_backend; };3.5 客户端:完全透明无感// client.cpp #include "ilogger.h" #include "remote_logger_adapter.h" class BusinessService { ILogger* m_log; public: explicit BusinessService(ILogger* log) : m_log(log) {} void doWork() { m_log->info("step 1"); // 客户端无需关心底层是本地还是远程 // ... 业务逻辑 ... m_log->error("failed"); // 同样透明 } }; int main() { // 运行时选择具体实现 auto remote = std::make_unique<RemoteLoggerAdapter>( std::make_unique<RemoteLogger>()); BusinessService svc(remote.get()); svc.doWork(); // 切换实现:只需更换适配器,业务代码零改动 QtDebugAdapter local; BusinessService svc2(&local); svc2.doWork(); return 0; }编译运行命令:g++ -std=c++17 -fPIC client.cpp -o adapter_demo $(pkg-config --cflags --libs Qt6Core) ./adapter_demo四、对象适配 vs 类适配:如何选择在 C++ 项目中,绝大多数场景应优先选用对象适配。维度对象适配(组合)类适配(多继承)实现方式class Adapter : public ITarget { Adaptee* m_src; }class Adapter : public ITarget, public Adaptee灵活性✅ 高(支持运行时动态切换被适配者)❌ 低(绑定于编译期)支持适配数量✅ 可组合多个适配者(多成员指针)❌ 仅能继承一个具体类(受限于单继承)风险几乎无⚠️ 存在菱形继承隐患Qt 项目推荐✅✅✅首选方案❌ 基本不使用// ✅ 推荐:对象适配(组合) class VendorCameraAdapter : public ICamera { VendorCamera* m_src; // 组合方式,清晰安全 }; // ❌ 不推荐:类适配(多继承 + 菱形风险) class VendorCameraAdapter : public ICamera, public VendorCamera { // 若 VendorCamera 本身有基类,可能引发菱形继承问题 };五、通用模板场景模板 1:统一日志门面即上文 3.4 节的实现,可抽象为通用结构:// 所有业务代码依赖统一接口 ILogger ILogger* log = selectLoggerFromConfig("prod"); // 根据配置选择具体适配器 BusinessService(log).doWork();核心收益:未来更换日志库时,业务代码无需修改,仅需替换适配器即可。模板 2:协议转换(MQTT ↔ Modbus ↔ HTTP)class IUnifiedProtocol { public: virtual QByteArray pack(const Command& cmd) = 0; virtual Command unpack(const QByteArray& raw) = 0; }; // 多个适配器实现统一接口 class MqttAdapter : public IUnifiedProtocol { /* 调用 libmosquitto */ }; class ModbusAdapter : public IUnifiedProtocol { /* 调用 libmodbus */ }; class HttpRestAdapter : public IUnifiedProtocol { /* 调用 QNetworkAccessManager */ };核心收益:业务层仅调用IUnifiedProtocol,切换通信协议等同于更换适配器,解耦彻底。模板 3:旧 API 兼容过渡项目升级至 Qt6 后,QRegExp已被QRegularExpression替代。但旧代码中仍有数千处调用QRegExp::exactMatch(...)。可通过适配器提供临时兼容层:// 过渡期兼容垫片,建议在 1-2 个版本后移除 class QRegExpAdapter { public: static bool exactMatch(const QString& str, const QString& pattern) { QRegularExpression re(pattern); return re.match(str).hasMatch(); } private: QRegularExpression m_re; }; // 老代码:QRegExpAdapter rx("^foo.*"); // rx.exactMatch("foobar"); // 依然可用💡复用思路:上述三种模板均遵循“定义一个统一目标接口 + 实现多个适配器”的模式。读者可将ILogger、IUnifiedProtocol替换为项目中的实际统一接口名。六、优缺点分析6.1 优点✅复用现有类:无需修改旧有代码(尤其在无法或不敢改动历史代码时极具价值)。✅实现解耦:客户端仅依赖目标接口,更换适配器无侵入性。✅统一多套接口:多个第三方 SDK → 统一接口 → 业务层只需编写一次逻辑。✅符合开闭原则:新增 SDK 只需添加新适配器,不影响已有代码。6.2 缺点❌类数量膨胀:每接入一个新系统就需新增一个适配器类。❌接口粒度难平衡:目标接口过细 → 适配器过多;过粗 → 失去适配意义。❌滥用导致混乱:本应重写却强行适配,使系统结构复杂化。❌依赖旧行为假设:适配器基于被适配者行为不变,一旦其变更,适配器也需同步调整。七、与其他设计模式对比模式关注点适用场景与适配器的区别单例全局唯一实例日志器、配置中心解决“实例数量”问题简单工厂快速创建对象产品少且稳定关注“选择哪个对象”工厂方法子类决定创建逻辑产品族可扩展同上抽象工厂一次性创建一组相关对象主题/界面套件同上Builder分步构建复杂对象零件固定但组合多样关注“构造顺序”Prototype克隆已有对象撤销重做、对象池关注“状态复制”Adapter接口转换集成旧系统 / 第三方 SDK关注“接口翻译”核心区别:前四者均为创建型模式(解决“如何创建”),而适配器属于结构型模式(解决“如何组合与转换”)。下一讲的桥接与装饰模式也同属结构型,将进一步探讨“对象组织方式”。八、最佳实践与避坑指南8.1 必须遵守🔸优先使用对象适配(组合方式),避免类适配(多重继承)。🔸适配器持有Adaptee的指针或智能指针,而非值类型——防止拷贝时意外复制被适配对象。🔸目标接口应保持稳定:一旦发布即不可更改,否则所有适配器需重写。🔸适配器应保持简洁:仅负责接口翻译(参数转换、错误码转异常),禁止掺杂业务逻辑。8.2 常见误区❌坑 1:适配器变成“万能处理器”// 错误示例:在适配器中嵌入业务逻辑 class CameraAdapter : public ICamera { public: QImage grab() override { if (m_src->Capture(...) != 0) { retry(); // ← 业务逻辑! log_error(); // ← 业务逻辑! fallback(); // ← 业务逻辑! } } };✅正确做法:适配器只做翻译,错误处理与重试交由业务层:QImage CameraAdapter::grab() override { QByteArray buf; int size = 0; int rc = m_src->Capture(reinterpret_cast<unsigned char*>(buf.data()), &size); if (rc != 0) return {}; // 失败返回空,由业务层决定后续动作 return QImage::fromData(buf.left(size)); }❌坑 2:目标接口粒度过细// 错误示例:每个操作都暴露为虚函数 class ICamera { public: virtual void powerOn() = 0; virtual void powerOff() = 0; virtual void setExposure() = 0; virtual void setGain() = 0; virtual void setWhiteBalance() = 0; virtual void trigger() = 0; virtual QImage grab() = 0; // ... 共 50 个虚函数 };✅正确做法:合并为 2~3 个粗粒度接口:class ICamera { public: virtual bool configure(const CameraConfig& cfg) = 0; // 批量配置 virtual QImage grab() = 0; // 核心业务操作 virtual void close() = 0; };❌坑 3:适配器与被适配者生命周期不一致// 错误示例:栈对象传入适配器,析构后悬空 int main() { VendorCamera vendor; // 栈对象 VendorCameraAdapter adapter(&vendor); // 持有指针 useAdapter(&adapter); // OK } // vendor 先析构,adapter 使用时已悬空?✅正确做法:使用std::unique_ptr明确所有权,“谁创建,谁管理”:int main() { auto vendor = std::make_unique<VendorCamera>(); auto adapter = std::make_unique<VendorCameraAdapter>(vendor.get()); // adapter 析构时,vendor 仍有效 // 确保 vendor 析构前,adapter 已先释放 }❌坑 4:目标接口未声明为抽象基类// 错误示例:目标接口是具体类,无法扩展 class Camera {}; // 无 virtual,无 = 0✅正确做法:目标接口必须具备虚析构和纯虚函数:class ICamera { public: virtual ~ICamera() = default; // ← 关键 virtual bool open() = 0; // ← 关键 // ... };九、写在最后适配器模式是应对“已有类无法直接接入新系统”问题的标准解决方案。在 Qt 项目中,任何涉及第三方 SDK 接入、老 API 兼容、多协议统一的场景,几乎都离不开它。一句话总结:别改老代码,给它贴一层翻译膜。如果本文对你有帮助,欢迎点赞、收藏、关注三连支持~下期预告:桥接模式(Bridge Pattern)——更深层次的解耦:将“抽象”与“实现”拆分为两个独立变化的维度。内容已在撰写中,敬请期待!完整可编译示例代码(含 Qt .pro 工程)已上传,回复关键词Adapter Demo即可获取。
系列文章:单例模式:【C++/Qt 进阶】设计模式系列(1):单例模式——饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客工厂模式与抽象工厂模式:【C++/Qt 进阶】设计模式系列(2):工厂模式——从游戏角色到披萨店-CSDN博客建造者模式:【C++/Qt 进阶】设计模式系列(3): 建造者模式——把“复杂对象的组装“从业务代码里剥离出来-CSDN博客原型模式:【C++/Qt 进阶】设计模式系列(4):原型模式——用“克隆“代替“重新构造“-CSDN博客适配器模式(本篇)桥接模式(持续更新中)写在前面⚠️ 注意:前四篇(单例 / 工厂 / 建造者 / 原型)均为创建型模式;从本篇起正式进入结构型模式的第一站——适配器。上一讲我们通过原型模式解决了“复制当前对象”的问题。但在真实的 Qt 项目中,更常见的挑战是:“我手头有一个现成的类(可能是第三方库,也可能是遗留代码),它的接口与系统需求不匹配,且无法修改源码。该怎么办?”举个典型的 Qt 工程师都遇到过的场景——日志系统迁移:// 旧代码:全量使用 qDebug() void SomeService::doWork() { qDebug() << "step 1"; // 输出到 stderr // ... 大量业务逻辑 ... qDebug() << "done"; }如今需要替换为企业级日志库(如 spdlog、QsLog,或自研 logger)。但项目中有 200 多个.cpp文件都直接调用qDebug(),逐个修改?恐怕要改到明年。另一个常见场景——第三方 SDK 接口风格与项目规范不符:// 第三方相机接口(错误码风格) class VendorCamera { public: int Open(const char* dev); int Capture(unsigned char* buf, int* size); void Close(); }; // 项目统一接口(Qt 风格) class ICamera { public: virtual ~ICamera() = default; virtual bool open() = 0; virtual QImage grab() = 0; virtual void close() = 0; };若直接使用VendorCamera,所有调用方都得重复编写if (rc != 0) handle_error(...)的样板代码。适配器模式正是为此类“接口不兼容”问题提供一层“翻译层”。记忆口诀:“两头不对,中间一塞”——目标接口、适配者、客户端各司其职,适配器作为中间翻译者,打通沟通壁垒。一、30 秒看懂适配器模式维度工厂模式(造新车)适配器模式(贴翻译膜)解决什么创建对象接口转换关注重点选哪种类型让“不兼容”的类能协作适用时机字段少、构造简单接入老系统 / 第三方 SDK典型应用产品族 / 套件日志封装 / 协议转换 / SDK 包装一句话总结:不修改现有类,让它能被新系统“翻译”着使用。在 C++ 中,通常采用对象适配(组合被适配者指针),少数情况下使用类适配(公有继承被适配者并实现目标接口)。二、UML 类图与角色以下是基于纯 C++/Qt 视角的适配器模式结构图,采用 Qt 容器、std::unique_ptr与虚函数声明的典型写法。┌──────────────────────────────┐ │ class ICamera │ ← 目标接口(Target) │ { │ 客户端期望的统一接口 │ public: │ │ virtual ~ICamera() = default; │ virtual bool open() │ │ = 0; │ │ virtual QImage grab() │ │ = 0; │ │ virtual void close() = 0; │ │ }; │ └───────────────┬──────────────┘ │ implements / 继承 + 实现 ▼ ┌──────────────────────────────┐ │ class VendorCameraAdapter : │ ← 适配器(Adapter) │ public ICamera │ │ { │ │ public: │ │ explicit VendorCameraAdapter( │ VendorCamera* src) │ 构造时注入被适配者 │ : m_src(src) {} │ │ bool open() override; │ 翻译:错误码 → bool │ QImage grab() override; │ 翻译:原始数据 → QImage │ void close() override; │ 翻译:返回值 → void │ private: │ │ std::unique_ptr<VendorCamera> m_src; │ ← 持有被适配者(组合) │ }; │ └───────────────┬──────────────┘ │ 持有 ▼ ┌──────────────────────────────┐ │ class VendorCamera │ ← 适配者(Adaptee) │ { │ 已有或第三方类,接口不兼容 │ public: │ │ int Open(const char* dev); │ │ int Capture(unsigned char*, int* size); │ void Close(); │ │ }; │ └──────────────────────────────┘ ▲ │ 调用 ┌───────────────┴──────────────┐ │ class CameraClient │ ← 客户端(Client) │ { │ 只依赖 ICamera 接口 │ void shoot(ICamera* cam); │ │ }; │ └──────────────────────────────┘三个核心角色角色职责C++/Qt 中的体现目标接口(Target)客户端期望的统一接口class ICamera { virtual open()/grab()/close() = 0; }适配者(Adaptee)已存在但接口不兼容的类(第三方或旧代码)VendorCamera(int Open()风格)适配器(Adapter)实现目标接口,内部组合适配者,完成接口翻译VendorCameraAdapter💡C++ 特性提示:相比 Java 的extends继承方式,C++ 更倾向于使用组合而非继承来实现适配器,以避免多重继承带来的复杂性和耦合风险。因此,对象适配是主流选择。三、基础代码实现(C++/Qt 版)以下通过一个 Qt 工程师最熟悉的“统一日志门面”示例,将qDebug()、std::cout与假想的RemoteLogger三种日志实现统一为一个ILogger接口。3.1 目标接口// ilogger.h #include <QString> class ILogger { public: virtual ~ILogger() = default; virtual void info(const QString& msg) = 0; virtual void warn(const QString& msg) = 0; virtual void error(const QString& msg) = 0; };3.2 适配者 1:Qt 内置qDebug()// qdebuglogger.h #include <QDebug> #include "ilogger.h" // 适配者:Qt 风格,无类仅函数 // 使用空类包装,使其与其他适配者对齐 class QtDebugBackend { public: void output(QtMsgType level, const QString& msg) { qDebug().noquote() << msg; } };3.3 适配者 2:远程日志服务// remotelogger.h #include <QString> #include <QNetworkAccessManager> // 适配者:第三方团队提供的 HTTP 日志 SDK class RemoteLogger { public: int sendLog(const char* level, const char* msg) { return postHttp(level, msg); } private: int postHttp(const char*, const char*); };3.4 适配器:统一前两者至ILogger// remote_logger_adapter.h #include "ilogger.h" #include "remotelogger.h" #include <memory> // 对象适配:组合式封装 RemoteLogger class RemoteLoggerAdapter : public ILogger { public: explicit RemoteLoggerAdapter(std::unique_ptr<RemoteLogger> logger) : m_logger(std::move(logger)) {} void info(const QString& msg) override { if (m_logger->sendLog("INFO", msg.toUtf8().constData()) != 0) { // 失败兜底:降级到本地输出 qWarning() << "[fallback]" << msg; } } void warn(const QString& msg) override { m_logger->sendLog("WARN", msg.toUtf8().constData()); } void error(const QString& msg) override { m_logger->sendLog("ERROR", msg.toUtf8().constData()); } private: std::unique_ptr<RemoteLogger> m_logger; }; // 另一适配器:将 QtDebugBackend 包装为 ILogger class QtDebugAdapter : public ILogger { public: void info(const QString& msg) override { m_backend.output(QtInfoMsg, msg); } void warn(const QString& msg) override { m_backend.output(QtWarningMsg, msg); } void error(const QString& msg) override { m_backend.output(QtCriticalMsg, msg); } private: QtDebugBackend m_backend; };3.5 客户端:完全透明无感// client.cpp #include "ilogger.h" #include "remote_logger_adapter.h" class BusinessService { ILogger* m_log; public: explicit BusinessService(ILogger* log) : m_log(log) {} void doWork() { m_log->info("step 1"); // 客户端无需关心底层是本地还是远程 // ... 业务逻辑 ... m_log->error("failed"); // 同样透明 } }; int main() { // 运行时选择具体实现 auto remote = std::make_unique<RemoteLoggerAdapter>( std::make_unique<RemoteLogger>()); BusinessService svc(remote.get()); svc.doWork(); // 切换实现:只需更换适配器,业务代码零改动 QtDebugAdapter local; BusinessService svc2(&local); svc2.doWork(); return 0; }编译运行命令:g++ -std=c++17 -fPIC client.cpp -o adapter_demo $(pkg-config --cflags --libs Qt6Core) ./adapter_demo四、对象适配 vs 类适配:如何选择在 C++ 项目中,绝大多数场景应优先选用对象适配。维度对象适配(组合)类适配(多继承)实现方式class Adapter : public ITarget { Adaptee* m_src; }class Adapter : public ITarget, public Adaptee灵活性✅ 高(支持运行时动态切换被适配者)❌ 低(绑定于编译期)支持适配数量✅ 可组合多个适配者(多成员指针)❌ 仅能继承一个具体类(受限于单继承)风险几乎无⚠️ 存在菱形继承隐患Qt 项目推荐✅✅✅首选方案❌ 基本不使用// ✅ 推荐:对象适配(组合) class VendorCameraAdapter : public ICamera { VendorCamera* m_src; // 组合方式,清晰安全 }; // ❌ 不推荐:类适配(多继承 + 菱形风险) class VendorCameraAdapter : public ICamera, public VendorCamera { // 若 VendorCamera 本身有基类,可能引发菱形继承问题 };五、通用模板场景模板 1:统一日志门面即上文 3.4 节的实现,可抽象为通用结构:// 所有业务代码依赖统一接口 ILogger ILogger* log = selectLoggerFromConfig("prod"); // 根据配置选择具体适配器 BusinessService(log).doWork();核心收益:未来更换日志库时,业务代码无需修改,仅需替换适配器即可。模板 2:协议转换(MQTT ↔ Modbus ↔ HTTP)class IUnifiedProtocol { public: virtual QByteArray pack(const Command& cmd) = 0; virtual Command unpack(const QByteArray& raw) = 0; }; // 多个适配器实现统一接口 class MqttAdapter : public IUnifiedProtocol { /* 调用 libmosquitto */ }; class ModbusAdapter : public IUnifiedProtocol { /* 调用 libmodbus */ }; class HttpRestAdapter : public IUnifiedProtocol { /* 调用 QNetworkAccessManager */ };核心收益:业务层仅调用IUnifiedProtocol,切换通信协议等同于更换适配器,解耦彻底。模板 3:旧 API 兼容过渡项目升级至 Qt6 后,QRegExp已被QRegularExpression替代。但旧代码中仍有数千处调用QRegExp::exactMatch(...)。可通过适配器提供临时兼容层:// 过渡期兼容垫片,建议在 1-2 个版本后移除 class QRegExpAdapter { public: static bool exactMatch(const QString& str, const QString& pattern) { QRegularExpression re(pattern); return re.match(str).hasMatch(); } private: QRegularExpression m_re; }; // 老代码:QRegExpAdapter rx("^foo.*"); // rx.exactMatch("foobar"); // 依然可用💡复用思路:上述三种模板均遵循“定义一个统一目标接口 + 实现多个适配器”的模式。读者可将ILogger、IUnifiedProtocol替换为项目中的实际统一接口名。六、优缺点分析6.1 优点✅复用现有类:无需修改旧有代码(尤其在无法或不敢改动历史代码时极具价值)。✅实现解耦:客户端仅依赖目标接口,更换适配器无侵入性。✅统一多套接口:多个第三方 SDK → 统一接口 → 业务层只需编写一次逻辑。✅符合开闭原则:新增 SDK 只需添加新适配器,不影响已有代码。6.2 缺点❌类数量膨胀:每接入一个新系统就需新增一个适配器类。❌接口粒度难平衡:目标接口过细 → 适配器过多;过粗 → 失去适配意义。❌滥用导致混乱:本应重写却强行适配,使系统结构复杂化。❌依赖旧行为假设:适配器基于被适配者行为不变,一旦其变更,适配器也需同步调整。七、与其他设计模式对比模式关注点适用场景与适配器的区别单例全局唯一实例日志器、配置中心解决“实例数量”问题简单工厂快速创建对象产品少且稳定关注“选择哪个对象”工厂方法子类决定创建逻辑产品族可扩展同上抽象工厂一次性创建一组相关对象主题/界面套件同上Builder分步构建复杂对象零件固定但组合多样关注“构造顺序”Prototype克隆已有对象撤销重做、对象池关注“状态复制”Adapter接口转换集成旧系统 / 第三方 SDK关注“接口翻译”核心区别:前四者均为创建型模式(解决“如何创建”),而适配器属于结构型模式(解决“如何组合与转换”)。下一讲的桥接与装饰模式也同属结构型,将进一步探讨“对象组织方式”。八、最佳实践与避坑指南8.1 必须遵守🔸优先使用对象适配(组合方式),避免类适配(多重继承)。🔸适配器持有Adaptee的指针或智能指针,而非值类型——防止拷贝时意外复制被适配对象。🔸目标接口应保持稳定:一旦发布即不可更改,否则所有适配器需重写。🔸适配器应保持简洁:仅负责接口翻译(参数转换、错误码转异常),禁止掺杂业务逻辑。8.2 常见误区❌坑 1:适配器变成“万能处理器”// 错误示例:在适配器中嵌入业务逻辑 class CameraAdapter : public ICamera { public: QImage grab() override { if (m_src->Capture(...) != 0) { retry(); // ← 业务逻辑! log_error(); // ← 业务逻辑! fallback(); // ← 业务逻辑! } } };✅正确做法:适配器只做翻译,错误处理与重试交由业务层:QImage CameraAdapter::grab() override { QByteArray buf; int size = 0; int rc = m_src->Capture(reinterpret_cast<unsigned char*>(buf.data()), &size); if (rc != 0) return {}; // 失败返回空,由业务层决定后续动作 return QImage::fromData(buf.left(size)); }❌坑 2:目标接口粒度过细// 错误示例:每个操作都暴露为虚函数 class ICamera { public: virtual void powerOn() = 0; virtual void powerOff() = 0; virtual void setExposure() = 0; virtual void setGain() = 0; virtual void setWhiteBalance() = 0; virtual void trigger() = 0; virtual QImage grab() = 0; // ... 共 50 个虚函数 };✅正确做法:合并为 2~3 个粗粒度接口:class ICamera { public: virtual bool configure(const CameraConfig& cfg) = 0; // 批量配置 virtual QImage grab() = 0; // 核心业务操作 virtual void close() = 0; };❌坑 3:适配器与被适配者生命周期不一致// 错误示例:栈对象传入适配器,析构后悬空 int main() { VendorCamera vendor; // 栈对象 VendorCameraAdapter adapter(&vendor); // 持有指针 useAdapter(&adapter); // OK } // vendor 先析构,adapter 使用时已悬空?✅正确做法:使用std::unique_ptr明确所有权,“谁创建,谁管理”:int main() { auto vendor = std::make_unique<VendorCamera>(); auto adapter = std::make_unique<VendorCameraAdapter>(vendor.get()); // adapter 析构时,vendor 仍有效 // 确保 vendor 析构前,adapter 已先释放 }❌坑 4:目标接口未声明为抽象基类// 错误示例:目标接口是具体类,无法扩展 class Camera {}; // 无 virtual,无 = 0✅正确做法:目标接口必须具备虚析构和纯虚函数:class ICamera { public: virtual ~ICamera() = default; // ← 关键 virtual bool open() = 0; // ← 关键 // ... };九、写在最后适配器模式是应对“已有类无法直接接入新系统”问题的标准解决方案。在 Qt 项目中,任何涉及第三方 SDK 接入、老 API 兼容、多协议统一的场景,几乎都离不开它。一句话总结:别改老代码,给它贴一层翻译膜。如果本文对你有帮助,欢迎点赞、收藏、关注三连支持~下期预告:桥接模式(Bridge Pattern)——更深层次的解耦:将“抽象”与“实现”拆分为两个独立变化的维度。内容已在撰写中,敬请期待!完整可编译示例代码(含 Qt .pro 工程)已上传,回复关键词Adapter Demo即可获取。