【C++/Qt 进阶】设计模式系列(5):适配器模式——给“不兼容的接口“贴一层翻译膜

系列文章:

  1. 单例模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
  2. 工厂模式与抽象工厂模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
  3. 建造者模式:【C++/Qt 进阶】设计模式系列(3): 建造者模式------把"复杂对象的组装"从业务代码里剥离出来-CSDN博客
  4. 原型模式【C++/Qt 进阶】设计模式系列(2):工厂模式------从游戏角色到披萨店-CSDN博客
  5. 适配器模式(本篇)
  6. 桥接模式(持续更新中)

写在前面

⚠️ 注意:前四篇(单例 / 工厂 / 建造者 / 原型)均为创建型 模式;从本篇起正式进入结构型模式的第一站------适配器。

上一讲我们通过原型模式解决了"复制当前对象"的问题。但在真实的 Qt 项目中,更常见的挑战是:

"我手头有一个现成的类(可能是第三方库,也可能是遗留代码),它的接口与系统需求不匹配,且无法修改源码。该怎么办?"

举个典型的 Qt 工程师都遇到过的场景------日志系统迁移:

cpp 复制代码
// 旧代码:全量使用 qDebug()
void SomeService::doWork() {
    qDebug() << "step 1";  // 输出到 stderr
    // ... 大量业务逻辑 ...
    qDebug() << "done";
}

如今需要替换为企业级日志库(如 spdlog、QsLog,或自研 logger)。但项目中有 200 多个 .cpp 文件都直接调用 qDebug(),逐个修改?恐怕要改到明年。

另一个常见场景------第三方 SDK 接口风格与项目规范不符:

cpp 复制代码
// 第三方相机接口(错误码风格)
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 与虚函数声明的典型写法。

text 复制代码
          ┌──────────────────────────────┐
          │  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 目标接口

cpp 复制代码
// 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()

cpp 复制代码
// qdebuglogger.h
#include <QDebug>
#include "ilogger.h"

// 适配者:Qt 风格,无类仅函数
// 使用空类包装,使其与其他适配者对齐

class QtDebugBackend {
public:
    void output(QtMsgType level, const QString& msg) {
        qDebug().noquote() << msg;
    }
};

3.3 适配者 2:远程日志服务

cpp 复制代码
// 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

cpp 复制代码
// 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 客户端:完全透明无感

cpp 复制代码
// 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;
}

编译运行命令:

bash 复制代码
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 项目推荐 ✅✅✅ 首选方案 ❌ 基本不使用
cpp 复制代码
// ✅ 推荐:对象适配(组合)
class VendorCameraAdapter : public ICamera {
    VendorCamera* m_src;  // 组合方式,清晰安全
};

// ❌ 不推荐:类适配(多继承 + 菱形风险)
class VendorCameraAdapter : public ICamera, public VendorCamera {
    // 若 VendorCamera 本身有基类,可能引发菱形继承问题
};

五、通用模板场景

模板 1:统一日志门面

即上文 3.4 节的实现,可抽象为通用结构:

cpp 复制代码
// 所有业务代码依赖统一接口 ILogger
ILogger* log = selectLoggerFromConfig("prod");  // 根据配置选择具体适配器
BusinessService(log).doWork();

核心收益 :未来更换日志库时,业务代码无需修改,仅需替换适配器即可。


模板 2:协议转换(MQTT ↔ Modbus ↔ HTTP)

cpp 复制代码
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(...)。可通过适配器提供临时兼容层:

cpp 复制代码
// 过渡期兼容垫片,建议在 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:适配器变成"万能处理器"

cpp 复制代码
// 错误示例:在适配器中嵌入业务逻辑
class CameraAdapter : public ICamera {
public:
    QImage grab() override {
        if (m_src->Capture(...) != 0) {
            retry();           // ← 业务逻辑!
            log_error();       // ← 业务逻辑!
            fallback();        // ← 业务逻辑!
        }
    }
};

✅ 正确做法:适配器只做翻译,错误处理与重试交由业务层:

cpp 复制代码
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:目标接口粒度过细

cpp 复制代码
// 错误示例:每个操作都暴露为虚函数
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 个粗粒度接口:

cpp 复制代码
class ICamera {
public:
    virtual bool configure(const CameraConfig& cfg) = 0;  // 批量配置
    virtual QImage grab() = 0;                            // 核心业务操作
    virtual void close() = 0;
};

❌ 坑 3:适配器与被适配者生命周期不一致

cpp 复制代码
// 错误示例:栈对象传入适配器,析构后悬空
int main() {
    VendorCamera vendor;                    // 栈对象
    VendorCameraAdapter adapter(&vendor);   // 持有指针
    useAdapter(&adapter);                   // OK
} // vendor 先析构,adapter 使用时已悬空?

✅ 正确做法 :使用 std::unique_ptr 明确所有权,"谁创建,谁管理":

cpp 复制代码
int main() {
    auto vendor = std::make_unique<VendorCamera>();
    auto adapter = std::make_unique<VendorCameraAdapter>(vendor.get());
    // adapter 析构时,vendor 仍有效
    // 确保 vendor 析构前,adapter 已先释放
}

❌ 坑 4:目标接口未声明为抽象基类

cpp 复制代码
// 错误示例:目标接口是具体类,无法扩展
class Camera {};  // 无 virtual,无 = 0

✅ 正确做法:目标接口必须具备虚析构和纯虚函数:

cpp 复制代码
class ICamera {
public:
    virtual ~ICamera() = default;          // ← 关键
    virtual bool open() = 0;               // ← 关键
    // ...
};

九、写在最后

适配器模式是应对"已有类无法直接接入新系统 "问题的标准解决方案。在 Qt 项目中,任何涉及第三方 SDK 接入、老 API 兼容、多协议统一的场景,几乎都离不开它。

一句话总结:别改老代码,给它贴一层翻译膜。

如果本文对你有帮助,欢迎点赞、收藏、关注三连支持~

下期预告:桥接模式(Bridge Pattern)------更深层次的解耦:将"抽象"与"实现"拆分为两个独立变化的维度。内容已在撰写中,敬请期待!

完整可编译示例代码(含 Qt .pro 工程)已上传,回复关键词 Adapter Demo 即可获取。

相关推荐
aqiu1111111 小时前
蓝桥杯真题复盘:Fibonacci 数列与黄金分割(C++ 算法详解)
c++·算法·蓝桥杯
lojox1 小时前
C++ 手写 STL list(双向循环链表)原理剖析与实现
c++·链表·list
青少儿编程课堂1 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:拓扑排序课程表排课解析
c++·python·算法·bfs·信息学竞赛
暖焰核心2 小时前
二叉搜索树——从插入查到key-value改写
c++
草莓熊Lotso5 小时前
【C++ 基于微服务的即时通讯系统】etcd + brpc 从安装到服务注册发现全链路实战
网络·数据库·c++·redis·微服务·etcd
把我格子衫拿来12 小时前
VTK 9.5 与 Qt 6 联合开发:从嵌入三维窗口到可维护的桌面架构
c++·python·qt·vtk·三维可视化
无忧.芙桃12 小时前
C++语言原理与实践(十六):多态机制讲解
开发语言·c++·多态
wuminyu14 小时前
JMM学习总结:深入理解happens-before关系
java·linux·c语言·jvm·c++
xxwl58514 小时前
区域赛分类题解
c++·笔记·学习·算法