彻底理解C++中的适配器模式!

来源:码农的荒岛求生 嵌入式 11 次阅读
摘要:你是一名C++大牛,正在开发公司的核心业务系统,这个系统里有一套统一的日志接口: class ILogger { public:     virtual void log(const std::string& message) = 0;     virtual void error(const std::string& message) = 0; }; 所有模块都依赖这个接口,文件

你是一名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()改成writeLog()level参数。

更可怕的是,改完之后,你的系统就和这个第三方库绑死了,下次老板说"换一个更好的日志库",你是不是还要再改一遍?

就为了一个第三方库,改动整个系统的代码,代价太大,而且丧失了灵活性。

你需要一种方法:不改系统代码,也不改第三方代码,却能让它们协同工作。

写一个包装函数

你想到一个办法:在外面包一层函数,做接口转换。

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注册中心:

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);  // 调用继承来的方法
    }
    
    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_ 是基类指针,它可以指向任何子类对象!

// 同一个适配器类,适配不同的实现
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);

// 后来发现太耗电,换成低功耗版
adapter->setAdaptee(new LowPowerLogger());  // 运行时切换实现!

一个适配器类,适配了整个继承体系!

你对比了一下类适配器和对象适配器:类适配器中一个适配器只能绑定一个具体类;对象适配器通过持有基类指针,解决了这个问题!

这里的关键洞察是:

  • 继承是"我什么",编译时决定,绑定到具体类
  • 组合是"我什么",运行时可以指向基类的任意子类

这种"在两个不兼容接口之间搭建桥梁"的类,就是所谓的适配器(Adapter)。

图片

评论区

登录后即可参与讨论

立即登录