找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3192

积分

0

好友

475

主题
发表于 1 小时前 | 查看: 3| 回复: 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::stringconst 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,而不是 HighPerfLoggerLowPowerLogger

如果你想适配 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++ 乃至整个面向对象编程中,是解决接口复用和系统集成问题的经典手段。希望这个从实际开发困境出发的讲解,能帮助你在云栈社区和未来的项目中,更自如地运用这一强大工具。




上一篇:1958年夏:德州仪器工程师基尔比如何发明首个集成电路
下一篇:Java21虚拟线程实战:设计可自愈的永动消息推送引擎
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-15 12:26 , Processed in 0.644796 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表