你是一名 C++ 资深开发者,正在负责公司核心业务系统的开发。这个系统内部有一套统一的日志接口:
class ILogger {
public:
virtual void log(const std::string& message) = 0;
virtual void error(const std::string& message) = 0;
};
所有模块都依赖这个抽象接口。无论是文件日志、网络日志还是控制台日志,都通过实现 ILogger 来接入,从而实现了不同日志实现的无缝切换。
有一天,老板兴冲冲地找到你:“我搞到了一个性能超强的第三方日志库,据说比我们现在的方案快10倍,赶紧集成进去!”
你接过资料一看,第三方库的接口是这样的:
// 第三方库,只有头文件和编译好的.so文件
class FastLogger {
public:
void writeLog(const char* msg, int level);
};
你立刻意识到了问题。你们系统的接口使用 std::string,它用的是 const char*;你们有分开的 log() 和 error() 方法,它却用一个 level 参数来区分;你们的框架期望一个 ILogger* 指针,而它完全是一个不相干的类。
这两个接口根本对不上。这个库没法直接塞进你的系统。
修改自己的代码去适应第三方?
第一反应是:既然第三方库的代码改不了,那就修改我们自己的系统代码去适应它。
于是,你把所有调用 ILogger 的地方,都改成直接调用 FastLogger:
// 原来的代码
void processOrder(ILogger* logger){
logger->log("Processing order...");
// 业务逻辑
logger->error("Order failed!");
}
// 改成这样
void processOrder(FastLogger* logger){
logger->writeLog("Processing order...", 0);
// 业务逻辑
logger->writeLog("Order failed!", 1);
}
你改了第一个文件,然后是第二个,第三个…… 当改到第20个文件时,你快要崩溃了。系统里有上百处在使用 ILogger 接口。全部改完不仅耗时巨大,每一处还都得做 std::string 到 const char* 的转换,把 log()/error() 调用改成带 level 参数的 writeLog()。
更严重的问题是,改完之后,你的系统就和这个特定的第三方库强耦合了。如果下次老板又说“换一个更好的日志库”,你是不是还要把整个流程再重复一遍?
为了集成一个第三方库而大范围改动稳定的系统代码,代价太高,而且彻底丧失了灵活性。我们需要找到一种方法:不改动系统现有代码,也不修改第三方库,却能让它们协同工作。
写一个包装函数?
一个初步的想法是:在外面写一层包装函数,专门做接口转换。
FastLogger* g_logger = new FastLogger();
void logMessage(const std::string& message){
g_logger->writeLog(message.c_str(), 0);
}
void logError(const std::string& message){
g_logger->writeLog(message.c_str(), 1);
}
这样,系统代码调用 logMessage(),内部再转发给 FastLogger。测试一下,功能上确实能用。但很快,你遇到了新障碍。
你们的框架里有一个 Logger 注册中心,它要求注册的是 ILogger* 类型的对象指针:
class LoggerRegistry {
public:
void registerLogger(ILogger* logger){
loggers_.push_back(logger);
}
void broadcast(const std::string& msg){
for (auto* logger : loggers_) {
logger->log(msg);
}
}
private:
std::vector<ILogger*> loggers_;
};
registerLogger() 需要一个 ILogger* 指针,而你写的包装函数只是一组普通函数,不是对象,无法被注册进去!
LoggerRegistry registry;
registry.registerLogger(???); // 包装函数塞不进去
显然,简单的函数包装不够用。我们需要的是一个能作为对象参与到面向对象体系中的“翻译官”。
类适配器:用继承充当翻译
一个自然的想法是:创建一个新类。让它继承我们系统需要的 ILogger 接口,同时也继承第三方 FastLogger 的实现。
class LoggerAdapter : public ILogger, public FastLogger {
public:
void log(const std::string& message) override {
writeLog(message.c_str(), 0); // 调用从 FastLogger 继承来的方法
}
void error(const std::string& message) override {
writeLog(message.c_str(), 1);
}
};
这个 LoggerAdapter 类非常巧妙。对外,它是一个 ILogger,可以完美地注册到系统的框架里;对内,它又是一个 FastLogger,可以直接调用其日志写入功能。
现在来测试一下:
LoggerRegistry registry;
LoggerAdapter* adapter = new LoggerAdapter();
registry.registerLogger(adapter); // 成功注册!
registry.broadcast("Hello World"); // 日志成功写入!
太棒了!之前困扰你的接口不兼容问题,现在通过一个‘翻译类’完美解决了。
然而,一个月后,新的问题出现了。
类适配器的局限性
第三方供应商发布了新版本,除了原来的 FastLogger,还新增了两个针对不同场景优化的变种:
// 原来的日志类
class FastLogger {
public:
void writeLog(const char* msg, int level);
};
// 新增:高性能版(多线程优化)
class HighPerfLogger : public FastLogger {
public:
void writeLog(const char* msg, int level) override;
};
// 新增:低功耗版(移动端优化)
class LowPowerLogger : public FastLogger {
public:
void writeLog(const char* msg, int level) override;
};
你需要在服务器上使用 HighPerfLogger,在移动设备上使用 LowPowerLogger。问题来了:你之前写的类适配器是这样的:
class LoggerAdapter : public ILogger, public FastLogger { ... };
它继承的是 FastLogger,而不是 HighPerfLogger 或 LowPowerLogger。
如果你想适配 HighPerfLogger,必须再写一个新的适配器类:
class HighPerfLoggerAdapter : public ILogger, public HighPerfLogger { ... };
想适配 LowPowerLogger?还得再写一个:
class LowPowerLoggerAdapter : public ILogger, public LowPowerLogger { ... };
三个变种,就需要三个不同的适配器类。 未来供应商每增加一个变种,你就得跟着新增一个适配器类。类适配器做不到一个类适配整个继承体系,因为它的继承关系在编译期就写死了,缺乏灵活性。
我们需要一种更解耦、更灵活的方式。
对象适配器:用组合替代继承
灵光一闪:不用继承,用组合!
把被适配的对象作为成员变量持有,而不是作为父类去继承。
class LoggerAdapter : public ILogger {
private:
FastLogger* adaptee_; // 组合:持有一个被适配对象的指针
public:
LoggerAdapter(FastLogger* logger) : adaptee_(logger) {}
void log(const std::string& message) override {
adaptee_->writeLog(message.c_str(), 0);
}
void error(const std::string& message) override {
adaptee_->writeLog(message.c_str(), 1);
}
};
这里的关键变化是:
- 之前(类适配器):
LoggerAdapter 是一个 FastLogger (通过继承,绑定到了具体类)。
- 现在(对象适配器):
LoggerAdapter 有一个 FastLogger* (通过组合,持有一个基类指针)。
由于 adaptee_ 是基类 FastLogger 的指针,根据 C++ 的多态特性,它可以指向 FastLogger 及其任何子类的对象!
// 同一个适配器类,可以适配不同的具体实现
LoggerAdapter* adapter1 = new LoggerAdapter(new FastLogger());
LoggerAdapter* adapter2 = new LoggerAdapter(new HighPerfLogger());
LoggerAdapter* adapter3 = new LoggerAdapter(new LowPowerLogger());
// 甚至可以运行时动态切换!
FastLogger* currentImpl = new HighPerfLogger();
LoggerAdapter* adapter = new LoggerAdapter(currentImpl);
// 运行一段时间后,发现移动端耗电太快,切换为低功耗版
// (假设提供了set方法) adapter->setAdaptee(new LowPowerLogger()); // 运行时切换实现!
一个适配器类,现在可以适配整个继承体系的所有类!
我们来对比一下这两种实现 适配器模式 的方式:
- 类适配器:采用多重继承。一个适配器类只能绑定一个具体的被适配类,不够灵活,但有时能直接重写被适配类的方法。
- 对象适配器:采用组合。适配器持有被适配对象的引用或指针。一个适配器类可以适配被适配类及其所有子类,灵活性更高,更符合“组合优于继承”的设计模式原则。
这里的核心洞察是:
- 继承 代表“我是 (is-a) 什么”,关系在编译时确定,绑定到具体类。
- 组合 代表“我有 (has-a) 什么”,关系在运行时确定,可以指向基类的任意子类。
这种“在两个不兼容的接口之间充当桥梁”的类,就是我们今天讨论的 适配器 (Adapter)。它在 C++ 乃至整个面向对象编程中,是解决接口复用和系统集成问题的经典手段。希望这个从实际开发困境出发的讲解,能帮助你在云栈社区和未来的项目中,更自如地运用这一强大工具。