系列文章:
写在前面
⚠️ 注意:前四篇(单例 / 工厂 / 建造者 / 原型)均为创建型 模式;从本篇起正式进入结构型模式的第一站------适配器。
上一讲我们通过原型模式解决了"复制当前对象"的问题。但在真实的 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 即可获取。