目录
案例 2:第三方库整合 ——spdlog 日志库适配项目统一接口
代码实现(需先安装 spdlog:sudo apt install libspdlog-dev)
案例 3:多数据源适配 —— 统一 MySQL 和 Redis 的数据查询接口

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
一、从旅行充电说起:为啥我们需要适配器模式?

去欧洲旅行时,你带了笔记本电脑和手机充电器,到酒店才发现插座是两孔圆头的,而你的充电器是国标三孔插头 —— 这时候一个多功能转换插头救了急。这个转换插头不改变电压,只是把 “不兼容的接口” 转换成 “能用的接口”,这就是适配器最核心的作用。
软件开发中,我们每天都会遇到类似的 “接口不兼容” 问题:
- 旧系统升级时,遗留模块的接口和新系统不匹配,但旧模块稳定可靠,不能重构;
- 接入第三方库时,比如用 spdlog 做日志、用 OpenCV 处理图像,它们的接口和项目统一接口不一致;
- 多数据源整合时,MySQL、Redis、文件存储的查询接口五花八门,需要统一调用方式。
这时候适配器模式就成了代码里的 “转换插头”,它能在不修改原有代码的前提下,让两个接口不兼容的类协同工作。用官方定义说,适配器模式(Adapter Pattern)是结构型设计模式的一种,核心是 “接口转换”,让原本因接口不匹配无法协作的类能够无缝配合。
但很多开发者觉得设计模式 “虚”,其实不是模式没用,而是没找到贴近实际的用法。这篇文章就从 C++ 实战出发,用 3 个工业级案例带你吃透适配器模式,不管是面试还是项目开发,都能直接用。
二、适配器模式的核心原理:3 个角色 + 2 种实现

在讲代码之前,我们先理清适配器模式的 3 个核心角色,就像搞懂 “转换插头” 的组成部分:
- 目标接口(Target):客户端期望的统一接口,比如新系统的日志接口、支付接口;
- 适配者(Adaptee):已有但接口不兼容的类 / 模块,比如旧系统模块、第三方库、遗留代码;
- 适配器(Adapter):核心转换层,实现目标接口,同时持有适配者的引用或继承适配者,把目标接口的调用转换成适配者的调用。
适配器模式在 C++ 中有两种经典实现方式,分别对应 “继承” 和 “组合” 两种编程思想,各有适用场景。
2.1 先看 UML 类图:直观理解三者关系
类适配器 UML 图(继承实现)

对象适配器 UML 图(组合实现)

2.2 两种实现方式的 C++ 基础示例
我们用 “电源充电” 这个最直观的场景,写两个最小化可运行示例,对比两种实现的差异。
场景定义
- 目标接口(Target):
Voltage5V,提供 5V 电压(手机充电需要); - 适配者(Adaptee):
Voltage220V,家庭电路 220V 电压(接口不兼容); - 适配器(Adapter):把 220V 转换成 5V。
1. 类适配器:多继承实现(C++ 专属特性)
类适配器通过多继承实现:适配器同时继承目标接口和适配者类,重写目标接口的方法,内部调用适配者的方法。
#include <iostream>
using namespace std;
// 目标接口:客户端需要的5V电压接口
class Voltage5V {
public:
virtual ~Voltage5V() = default; // 虚析构,避免内存泄漏
virtual int output5V() const = 0; // 纯虚函数,定义统一接口
};
// 适配者:已有但接口不兼容的220V电压类
class Voltage220V {
public:
int output220V() const {
return 220; // 实际场景中是硬件提供的电压
}
};
// 类适配器:继承目标接口和适配者类
class VoltageAdapter : public Voltage5V, private Voltage220V {
public:
int output5V() const override {
// 1. 调用适配者的方法获取原始电压
int input = output220V();
// 2. 转换逻辑:220V -> 5V(实际是电源适配器的降压电路)
int output = input / 44;
cout << "电压转换:" << input << "V -> " << output << "V" << endl;
return output;
}
};
// 客户端:手机(只认5V接口)
class Phone {
public:
void charging(const Voltage5V& voltage) {
if (voltage.output5V() == 5) {
cout << "电压正常,手机开始充电~" << endl;
} else {
cout << "电压不兼容,无法充电!" << endl;
}
}
};
// 测试代码
int main() {
cout << "=== 类适配器模式测试 ===" << endl;
VoltageAdapter adapter;
Phone phone;
phone.charging(adapter); // 客户端无需关心转换细节
return 0;
}
运行结果:
=== 类适配器模式测试 ===
电压转换:220V -> 5V
电压正常,手机开始充电~
2. 对象适配器:组合实现(更推荐)
对象适配器通过组合实现:适配器只继承目标接口,内部持有适配者的指针 / 引用,通过构造函数注入适配者对象。这种方式更符合 “合成复用原则”,灵活性更高。
#include <iostream>
using namespace std;
// 目标接口:和类适配器一致
class Voltage5V {
public:
virtual ~Voltage5V() = default;
virtual int output5V() const = 0;
};
// 适配者:和类适配器一致
class Voltage220V {
public:
int output220V() const {
return 220;
}
};
// 对象适配器:持有适配者对象(组合)
class VoltageAdapter : public Voltage5V {
private:
Voltage220V* m_adaptee; // 持有适配者指针,支持动态替换
public:
// 构造函数注入适配者,依赖注入思想
explicit VoltageAdapter(Voltage220V* adaptee) : m_adaptee(adaptee) {}
~VoltageAdapter() override {
delete m_adaptee; // 释放适配者对象,避免内存泄漏
}
int output5V() const override {
if (!m_adaptee) {
throw runtime_error("适配者对象未初始化!");
}
int input = m_adaptee->output220V();
int output = input / 44;
cout << "电压转换(对象适配器):" << input << "V -> " << output << "V" << endl;
return output;
}
};
// 客户端:和类适配器一致
class Phone {
public:
void charging(const Voltage5V& voltage) {
if (voltage.output5V() == 5) {
cout << "电压正常,手机开始充电~" << endl;
} else {
cout << "电压不兼容,无法充电!" << endl;
}
}
};
// 测试代码
int main() {
cout << "=== 对象适配器模式测试 ===" << endl;
Voltage220V* adaptee = new Voltage220V();
VoltageAdapter adapter(adaptee);
Phone phone;
phone.charging(adapter);
return 0;
}
运行结果:
=== 对象适配器模式测试 ===
电压转换(对象适配器):220V -> 5V
电压正常,手机开始充电~
2.3 两种实现方式对比:该怎么选?
很多开发者纠结选哪种实现,其实核心看场景,用表格总结更清晰:
| 对比维度 | 类适配器(多继承) | 对象适配器(组合) |
|---|---|---|
| 实现方式 | 继承 Target 和 Adaptee | 继承 Target,组合 Adaptee |
| 灵活性 | 低:静态绑定,无法动态替换 Adaptee | 高:运行时可替换 Adaptee,支持多适配 |
| 访问权限 | 可访问 Adaptee 的 protected 成员 | 只能访问 Adaptee 的 public 成员 |
| 耦合度 | 高:适配器与 Adaptee 强绑定 | 低:通过接口交互,解耦效果好 |
| 适用场景 | Adaptee 稳定、接口简单,无多态需求 | Adaptee 易变、需多适配,追求高内聚 |
实际开发建议:优先选对象适配器!因为组合比继承更灵活,符合 “合成复用原则”,后续修改 Adaptee 时,适配器的改动最小,这也是工业级项目中最常用的方式。
三、3 个工业级 C++ 案例:从理论到实战
基础示例只能帮我们理解原理,真正的价值在于解决实际问题。下面 3 个案例覆盖了 “遗留系统改造”“第三方库整合”“多数据源适配” 三大高频场景,代码可直接复制到项目中使用。
案例 1:遗留系统改造 —— 旧日志模块适配新系统接口

场景描述
公司旧系统有一个日志模块LegacyLogger,接口是logToFile(const string& msg, int level),已经在线上稳定运行 5 年,不能修改。新系统需要统一日志接口ILogger,包含info(const string& msg)、warn(const string& msg)、error(const string& msg)三个方法,要求不修改旧代码,让新系统能调用旧日志模块。
代码实现
#include <iostream>
#include <string>
using namespace std;
// 1. 目标接口:新系统统一日志接口
class ILogger {
public:
virtual ~ILogger() = default;
virtual void info(const string& msg) = 0; // 信息日志
virtual void warn(const string& msg) = 0; // 警告日志
virtual void error(const string& msg) = 0; // 错误日志
};
// 2. 适配者:遗留日志模块(不可修改)
class LegacyLogger {
public:
// 旧接口:msg是日志内容,level是级别(1=info,2=warn,3=error)
void logToFile(const string& msg, int level) {
string levelStr;
switch (level) {
case 1: levelStr = "[INFO]"; break;
case 2: levelStr = "[WARN]"; break;
case 3: levelStr = "[ERROR]"; break;
default: levelStr = "[UNKNOWN]";
}
cout << "LegacyLog: " << levelStr << " " << msg << " (写入文件)" << endl;
}
};
// 3. 适配器:将旧日志接口适配为新接口
class LegacyLoggerAdapter : public ILogger {
private:
LegacyLogger* m_legacyLogger; // 组合适配者对象
public:
LegacyLoggerAdapter() : m_legacyLogger(new LegacyLogger()) {}
~LegacyLoggerAdapter() override {
delete m_legacyLogger;
}
// 适配info日志:新接口 -> 旧接口
void info(const string& msg) override {
m_legacyLogger->logToFile(msg, 1);
}
// 适配warn日志
void warn(const string& msg) override {
m_legacyLogger->logToFile(msg, 2);
}
// 适配error日志
void error(const string& msg) override {
m_legacyLogger->logToFile(msg, 3);
}
};
// 4. 客户端:新系统业务代码(只依赖ILogger接口)
void businessLogic(ILogger& logger) {
logger.info("系统启动成功");
logger.warn("内存使用率超过80%");
logger.error("数据库连接失败");
}
// 测试代码
int main() {
cout << "=== 遗留日志模块适配测试 ===" << endl;
LegacyLoggerAdapter adapter;
businessLogic(adapter); // 新系统无缝调用旧日志模块
return 0;
}
运行结果
=== 遗留日志模块适配测试 ===
LegacyLog: [INFO] 系统启动成功 (写入文件)
LegacyLog: [WARN] 内存使用率超过80% (写入文件)
LegacyLog: [ERROR] 数据库连接失败 (写入文件)
核心价值
- 不修改旧代码,避免引入风险;
- 新系统业务代码无需关心日志实现,符合 “依赖倒置原则”;
- 后续如果要替换日志模块(比如换成 spdlog),只需新增一个适配器,无需修改业务逻辑。
案例 2:第三方库整合 ——spdlog 日志库适配项目统一接口

场景描述
项目中需要用 spdlog(一款高效的 C++ 日志库),但 spdlog 的接口是info(const char* fmt, ...)、error(const char* fmt, ...),而项目统一日志接口是ILogger(和案例 1 一致)。需要通过适配器让 spdlog 适配统一接口,方便后续替换日志库。
代码实现(需先安装 spdlog:sudo apt install libspdlog-dev)
#include <iostream>
#include <string>
#include <spdlog/spdlog.h>
using namespace std;
// 1. 目标接口:项目统一日志接口(和案例1一致)
class ILogger {
public:
virtual ~ILogger() = default;
virtual void info(const string& msg) = 0;
virtual void warn(const string& msg) = 0;
virtual void error(const string& msg) = 0;
};
// 2. 适配者:第三方库spdlog(不可修改)
// 注:spdlog是单例模式,这里直接使用其接口
// 3. 适配器:将spdlog接口适配为ILogger
class SpdLoggerAdapter : public ILogger {
public:
SpdLoggerAdapter() {
// 初始化spdlog:设置日志级别、输出格式
spdlog::set_level(spdlog::level::info);
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
}
void info(const string& msg) override {
spdlog::info(msg); // 调用spdlog的info接口
}
void warn(const string& msg) override {
spdlog::warn(msg); // 调用spdlog的warn接口
}
void error(const string& msg) override {
spdlog::error(msg); // 调用spdlog的error接口
}
};
// 4. 客户端:业务代码(和案例1一致,无需修改)
void businessLogic(ILogger& logger) {
logger.info("用户登录成功,用户名:admin");
logger.warn("用户连续3次密码错误");
logger.error("订单创建失败,数据库异常");
}
// 测试代码
int main() {
cout << "=== spdlog适配测试 ===" << endl;
SpdLoggerAdapter adapter;
businessLogic(adapter);
return 0;
}
编译命令
g++ -std=c++11 spdlog_adapter.cpp -o spdlog_adapter -lspdlog
运行结果
=== spdlog适配测试 ===
[2025-11-19 10:30:00] [info] 用户登录成功,用户名:admin
[2025-11-19 10:30:00] [warning] 用户连续3次密码错误
[2025-11-19 10:30:00] [error] 订单创建失败,数据库异常
核心价值
- 隔离第三方库与业务代码,降低耦合;
- 统一接口风格,让开发者无需记忆多种日志库接口;
- 后续替换日志库(比如换成 glog),只需新增
GlogLoggerAdapter,业务代码零改动。
案例 3:多数据源适配 —— 统一 MySQL 和 Redis 的数据查询接口

场景描述
项目中需要从 MySQL 和 Redis 查询数据,两者接口差异很大:
- MySQL 的查询接口:
queryMysql(const string& sql) -> string; - Redis 的查询接口:
getRedis(const string& key) -> string;需要适配成统一的数据查询接口IDataSource,提供getData(const string& param)方法,让业务代码能统一调用。
代码实现
#include <iostream>
#include <string>
#include <map>
using namespace std;
// 1. 目标接口:统一数据查询接口
class IDataSource {
public:
virtual ~IDataSource() = default;
virtual string getData(const string& param) = 0; // param:MySQL是SQL,Redis是key
};
// 2. 适配者1:MySQL数据库(模拟)
class MysqlDB {
public:
// MySQL查询接口:接收SQL语句,返回查询结果
string queryMysql(const string& sql) {
// 模拟MySQL查询逻辑
if (sql == "SELECT name FROM user WHERE id=1") {
return "用户ID=1,姓名:张三";
} else if (sql == "SELECT age FROM user WHERE id=1") {
return "用户ID=1,年龄:25";
} else {
return "MySQL查询无结果";
}
}
};
// 3. 适配者2:Redis数据库(模拟)
class RedisDB {
private:
map<string, string> m_data; // 模拟Redis数据存储
public:
RedisDB() {
// 初始化测试数据
m_data["user:1:name"] = "张三";
m_data["user:1:age"] = "25";
m_data["user:2:name"] = "李四";
}
// Redis查询接口:接收key,返回value
string getRedis(const string& key) {
auto it = m_data.find(key);
if (it != m_data.end()) {
return "Redis查询结果:" + it->second;
} else {
return "Redis查询无结果";
}
}
};
// 4. 适配器1:MySQL适配IDataSource
class MysqlAdapter : public IDataSource {
private:
MysqlDB* m_mysql;
public:
MysqlAdapter() : m_mysql(new MysqlDB()) {}
~MysqlAdapter() override {
delete m_mysql;
}
string getData(const string& param) override {
// param是SQL语句,直接传递给MySQL
return m_mysql->queryMysql(param);
}
};
// 5. 适配器2:Redis适配IDataSource
class RedisAdapter : public IDataSource {
private:
RedisDB* m_redis;
public:
RedisAdapter() : m_redis(new RedisDB()) {}
~RedisAdapter() override {
delete m_redis;
}
string getData(const string& param) override {
// param是Redis的key,直接传递给Redis
return m_redis->getRedis(param);
}
};
// 6. 客户端:业务代码(统一调用IDataSource)
void queryData(IDataSource& dataSource, const string& param) {
string result = dataSource.getData(param);
cout << "查询结果:" << result << endl;
}
// 测试代码
int main() {
cout << "=== 多数据源适配测试 ===" << endl;
// MySQL查询
MysqlAdapter mysqlAdapter;
cout << "\n--- MySQL查询 ---" << endl;
queryData(mysqlAdapter, "SELECT name FROM user WHERE id=1");
queryData(mysqlAdapter, "SELECT age FROM user WHERE id=1");
// Redis查询
RedisAdapter redisAdapter;
cout << "\n--- Redis查询 ---" << endl;
queryData(redisAdapter, "user:1:name");
queryData(redisAdapter, "user:2:name");
return 0;
}
运行结果
=== 多数据源适配测试 ===
--- MySQL查询 ---
查询结果:用户ID=1,姓名:张三
查询结果:用户ID=1,年龄:25
--- Redis查询 ---
查询结果:Redis查询结果:张三
查询结果:Redis查询结果:李四
核心价值
- 业务代码无需区分数据源类型,统一调用
getData方法; - 新增数据源(比如 MongoDB)时,只需新增
MongoAdapter,符合 “开闭原则”; - 便于切换数据源,比如把 Redis 换成 MySQL,只需替换适配器对象。
四、适配器模式的优缺点:不是万能的,要按需使用

4.1 优点
- 解决接口不兼容问题,实现代码复用:无需修改原有类(适配者),就能让其在新系统中工作;
- 解耦客户端与适配者:客户端只依赖目标接口,不关心适配者的具体实现,降低耦合度;
- 灵活性高:对象适配器支持动态替换适配者,还能适配多个适配者(比如一个适配器同时适配 MySQL 和 Redis);
- 符合 “开闭原则”:新增适配者时,只需新增适配器类,无需修改客户端和目标接口。
4.2 缺点
- 增加系统复杂度:引入适配器会多一层转换,增加代码量和理解成本;
- 转换开销:如果适配逻辑复杂(比如参数转换、格式解析),会影响系统性能;
- 不适用于频繁变化的接口:如果目标接口或适配者接口经常变动,适配器需要频繁修改,维护成本高。
4.3 适用场景总结
- 整合遗留系统:旧模块接口不兼容新系统,且旧模块无法修改;
- 接入第三方库:第三方库接口与项目统一接口不一致;
- 多组件协同:多个组件功能相似但接口不同,需要统一调用方式;
- 系统扩展:需要扩展新功能,但不想修改现有代码(符合开闭原则)。
五、避坑指南:适配器模式 vs 相似模式(面试高频)

很多开发者会把适配器模式和装饰器模式、代理模式搞混,这也是面试中的高频考点,用通俗的语言区分:
5.1 适配器模式 vs 装饰器模式
- 核心区别:适配器是 “改变接口”,装饰器是 “增强功能”;
- 适配器模式:两个不兼容的接口通过转换协同工作,不改变原有功能;
- 装饰器模式:在不改变接口的前提下,给对象动态添加新功能(比如给日志增加加密功能);
- 通俗比喻:适配器是 “转换插头”,装饰器是 “给手机加保护壳(增强功能,不改变充电接口)”。
5.2 适配器模式 vs 代理模式
- 核心区别:适配器是 “解决兼容性”,代理模式是 “控制访问”;
- 适配器模式:客户端不知道适配者的存在,只关心目标接口;
- 代理模式:客户端知道目标对象的存在,代理对象是为了控制对目标对象的访问(比如权限校验、日志记录);
- 通俗比喻:适配器是 “翻译官”,让不同语言的人沟通;代理模式是 “保安”,控制谁能进入大楼。
六、实际项目中的最佳实践
- 优先使用对象适配器:组合比继承更灵活,能避免多继承带来的菱形继承问题,也便于动态替换适配者;
- 目标接口要稳定:目标接口是客户端依赖的核心,尽量设计得简洁、稳定,避免频繁修改;
- 适配逻辑尽量简单:如果转换逻辑复杂,可拆分到单独的工具类(比如参数转换工具),让适配器只负责调用适配者;
- 命名规范:适配器类命名建议包含 “Adapter”,比如
MysqlAdapter、SpdLoggerAdapter,便于识别; - 避免过度使用:如果两个接口差异很小,直接修改其中一个接口更简单,无需引入适配器。
七、总结
适配器模式就像代码世界的 “万能转换器”,核心是 “接口转换”,让原本不兼容的类能够协同工作。它的价值不在于 “创造新功能”,而在于 “整合现有资源”—— 这在实际开发中非常重要,因为我们很少从零开始开发系统,更多是整合遗留代码、第三方库、多组件。
通过本文的 3 个工业级案例,相信你已经掌握了适配器模式的 C++ 实现和应用场景。记住:设计模式不是 “炫技工具”,而是 “解决问题的模板”,只有在合适的场景中使用,才能让代码更优雅、更易维护。
如果你的项目中遇到接口不兼容问题,不妨试试适配器模式 —— 它可能会让你少写很多重复代码,也能让系统更具扩展性。

被折叠的 条评论
为什么被折叠?



