欢迎光临
我们一直在努力

设计模式入门:3. 适配器模式详解 C++实现

适配器模式详解:让不兼容的接口携手工作,C++完整实现

引言

你有没有遇到过这样的情况:买了一个新手机,充电器却是Type-C接口,而你的充电宝只有USB-A接口?这时候你需要一个"转接头"——也就是适配器,来让两个不兼容的设备一起工作。

在软件开发中,我们也经常会遇到类似的问题:我们有一个已经存在的类,它的功能完全符合我们的需求,但它的接口却和我们当前系统的接口不兼容。如果直接修改这个类的接口,可能会影响到其他使用它的代码;如果重新写一个功能相同的类,又会造成代码重复。

适配器模式(Adapter Pattern) 正是为了解决这个问题而生的。它是一种结构型设计模式,能够将一个类的接口转换成客户端所期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类能够协同工作。

今天我们就用C++语言,从基础概念到具体实现,全面深入地理解适配器模式。


一、适配器模式的核心概念

1.1 什么是适配器模式

适配器模式的核心思想是:创建一个中间层类(适配器),作为两个不兼容接口之间的转换器。它包装了一个已有的对象,将其接口转换成客户端所需要的接口,从而让客户端可以透明地使用这个对象。

适配器模式就像一个翻译官,当两个说不同语言的人需要交流时,翻译官把一个人的语言翻译成另一个人能听懂的语言,让沟通得以顺利进行。

1.2 适配器模式的三个核心角色

适配器模式包含三个关键角色:

  • 目标接口(Target):客户端期望使用的接口,定义了客户端可以调用的方法
  • 适配者(Adaptee):已经存在的、需要被适配的类,它有我们需要的功能,但接口不兼容
  • 适配器(Adapter):中间转换类,它实现了目标接口,同时包装了一个适配者对象,将客户端对目标接口的调用转换为对适配者接口的调用
  • 1.3 适配器模式的两种主要类型

    根据适配器与适配者的关系,适配器模式可以分为两种:

    • 类适配器:适配器通过继承适配者类来实现适配
    • 对象适配器:适配器通过组合适配者对象来实现适配

    这两种方式各有优缺点,我们会在后面详细讲解。


    二、类适配器模式

    2.1 UML类图

    +—————-+ +—————-+
    | Client | —-> | Target | <– 目标接口
    +—————-+ +—————-+
    ^
    |
    +—————-+ +—————-+
    | Adaptee | <—- | Adapter | <– 适配器
    +—————-+ +—————-+

    类适配器通过多重继承同时继承目标接口和适配者类,然后在实现目标接口的方法时,调用继承自适配者类的方法。

    2.2 C++实现

    我们以"日志系统适配"为例来实现类适配器模式。假设我们有一个旧的日志类OldFileLogger,它提供了一个writeLog方法来写入日志。现在我们的新系统要求所有日志类都必须实现ILogger接口,该接口定义了一个log方法,支持不同的日志级别。

    #include <iostream>
    #include <string>

    // 目标接口:新系统要求的日志接口
    class ILogger {
    public:
    virtual ~ILogger() = default;
    virtual void log(const std::string& message, int level) const = 0;
    };

    // 适配者:旧的文件日志类,接口不兼容
    class OldFileLogger {
    public:
    void writeLog(const std::string& msg) const {
    std::cout << "[旧文件日志] " << msg << std::endl;
    }
    };

    // 类适配器:继承目标接口和适配者类
    class ClassAdapter : public ILogger, public OldFileLogger {
    public:
    // 实现目标接口的log方法
    void log(const std::string& message, int level) const override {
    // 将新接口的调用转换为旧接口的调用
    std::string formattedMsg = "级别" + std::to_string(level) + ": " + message;
    writeLog(formattedMsg); // 调用继承自适配者的方法
    }
    };

    // 客户端代码
    int main() {
    // 客户端只知道目标接口ILogger
    ILogger* logger = new ClassAdapter();

    // 客户端按照新接口的方式调用
    logger->log("系统启动成功", 1);
    logger->log("检测到异常连接", 3);

    delete logger;
    return 0;
    }

    2.3 运行结果

    [旧文件日志] 级别1: 系统启动成功
    [旧文件日志] 级别3: 检测到异常连接

    2.4 优缺点分析

    优点:

    • 可以重写适配者类的方法,灵活性更高
    • 不需要额外的指针来引用适配者对象,代码更简洁

    缺点:

    • 耦合度高:适配器继承了适配者类,违反了"优先使用组合而非继承"的设计原则
    • 只能适配一个适配者类:由于C++不支持多重继承多个普通类,所以类适配器只能继承一个适配者类
    • 可能会暴露适配者的内部方法:继承会将适配者的公有方法都暴露给适配器,可能会造成不必要的接口污染

    三、对象适配器模式

    3.1 UML类图

    +—————-+ +—————-+
    | Client | —-> | Target | <– 目标接口
    +—————-+ +—————-+
    ^
    |
    +—————-+
    | Adapter | <– 适配器
    +—————-+
    |
    v
    +—————-+
    | Adaptee | <– 适配者
    +—————-+

    对象适配器不继承适配者类,而是持有一个适配者对象的引用,然后在实现目标接口的方法时,调用这个引用指向的适配者对象的方法。

    3.2 C++实现

    我们还是用上面的日志系统例子,这次用对象适配器来实现。

    #include <iostream>
    #include <string>
    #include <memory>

    // 目标接口:新系统要求的日志接口
    class ILogger {
    public:
    virtual ~ILogger() = default;
    virtual void log(const std::string& message, int level) const = 0;
    };

    // 适配者:旧的文件日志类,接口不兼容
    class OldFileLogger {
    public:
    void writeLog(const std::string& msg) const {
    std::cout << "[旧文件日志] " << msg << std::endl;
    }
    };

    // 对象适配器:只继承目标接口,持有适配者对象的引用
    class ObjectAdapter : public ILogger {
    private:
    std::unique_ptr<OldFileLogger> adaptee_; // 组合适配者对象

    public:
    // 构造函数接收适配者对象
    explicit ObjectAdapter(std::unique_ptr<OldFileLogger> adaptee)
    : adaptee_(std::move(adaptee)) {}

    // 实现目标接口的log方法
    void log(const std::string& message, int level) const override {
    // 将新接口的调用转换为旧接口的调用
    std::string formattedMsg = "级别" + std::to_string(level) + ": " + message;
    adaptee_->writeLog(formattedMsg); // 调用组合的适配者对象的方法
    }
    };

    // 客户端代码
    int main() {
    // 创建适配者对象
    auto oldLogger = std::make_unique<OldFileLogger>();

    // 创建适配器,包装适配者对象
    ILogger* logger = new ObjectAdapter(std::move(oldLogger));

    // 客户端按照新接口的方式调用
    logger->log("系统启动成功", 1);
    logger->log("检测到异常连接", 3);

    delete logger;
    return 0;
    }

    3.3 运行结果

    和类适配器的运行结果完全相同:

    [旧文件日志] 级别1: 系统启动成功
    [旧文件日志] 级别3: 检测到异常连接

    3.4 优缺点分析

    优点:

    • 耦合度低:使用组合而非继承,符合设计原则
    • 可以适配多个适配者:一个适配器可以持有多个适配者对象,或者在运行时动态更换适配者
    • 不会暴露适配者的内部方法:只有适配器知道适配者的存在,客户端完全看不到适配者的接口

    缺点:

    • 不能重写适配者类的方法
    • 需要额外的指针来引用适配者对象,代码稍微复杂一点

    注意:在实际开发中,对象适配器是更推荐使用的方式,因为它的耦合度更低,灵活性更高。


    四、适配器模式的常见变种

    4.1 双向适配器

    双向适配器可以同时适配两个不同的接口,使得两个接口的客户端都可以通过适配器调用对方的方法。

    简单来说,双向适配器既是Target的适配器,也是Adaptee的适配器。它同时实现了Target和Adaptee的接口,让两个方向的调用都能正常工作。

    // 双向适配器
    class BidirectionalAdapter : public ILogger, public OldFileLogger {
    public:
    // 实现ILogger接口
    void log(const std::string& message, int level) const override {
    std::string formattedMsg = "级别" + std::to_string(level) + ": " + message;
    writeLog(formattedMsg);
    }

    // 也可以实现OldFileLogger的接口,让旧客户端调用新功能
    void writeLog(const std::string& msg) const override {
    // 这里可以调用新的日志功能
    std::cout << "[双向适配] 旧接口调用: " << msg << std::endl;
    }
    };

    4.2 缺省适配器(接口适配器)

    当一个接口有很多方法,而我们只需要实现其中的一部分时,可以使用缺省适配器。

    缺省适配器是一个抽象类,它为接口中的所有方法提供了一个空的默认实现。这样,我们只需要继承这个缺省适配器类,然后重写我们需要的方法即可,而不需要实现接口中的所有方法。

    // 一个有很多方法的接口
    class IComplexLogger {
    public:
    virtual ~IComplexLogger() = default;
    virtual void debug(const std::string& msg) = 0;
    virtual void info(const std::string& msg) = 0;
    virtual void warning(const std::string& msg) = 0;
    virtual void error(const std::string& msg) = 0;
    virtual void fatal(const std::string& msg) = 0;
    };

    // 缺省适配器:为所有方法提供空实现
    class DefaultLoggerAdapter : public IComplexLogger {
    public:
    void debug(const std::string& msg) override {}
    void info(const std::string& msg) override {}
    void warning(const std::string& msg) override {}
    void error(const std::string& msg) override {}
    void fatal(const std::string& msg) override {}
    };

    // 只需要实现我们关心的方法
    class SimpleErrorLogger : public DefaultLoggerAdapter {
    public:
    void error(const std::string& msg) override {
    std::cout << "[错误] " << msg << std::endl;
    }

    void fatal(const std::string& msg) override {
    std::cout << "[致命错误] " << msg << std::endl;
    exit(1);
    }
    };


    五、适配器模式的优缺点与适用场景

    5.1 优点

  • 提高了代码的复用性:不需要修改已有的类,就可以让它在新的系统中使用
  • 提高了系统的灵活性:可以在运行时动态更换适配器,从而改变系统的行为
  • 解耦了客户端和适配者:客户端只依赖目标接口,不需要知道适配者的存在
  • 符合开闭原则:添加新的适配器不需要修改现有代码
  • 5.2 缺点

  • 增加了系统的复杂度:引入了额外的适配器类,增加了代码的层数
  • 过多的适配器会让系统变得混乱:如果滥用适配器模式,会导致系统中出现大量的小类,增加理解和维护的难度
  • 5.3 适用场景

  • 想使用一个已经存在的类,但它的接口不符合你的需求
  • 想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作
  • 需要使用多个已经存在的子类,但不可能对每一个都进行子类化以匹配它们的接口
  • 需要统一多个不同接口的调用方式

  • 六、与其他模式的对比

    很多人容易把适配器模式和其他一些结构型模式混淆,这里我们来做一个清晰的对比:

    模式核心目的与适配器的区别
    适配器模式 转换接口,让不兼容的类能够一起工作 改变接口,不改变功能
    装饰器模式 动态地给对象添加额外的功能 不改变接口,增强功能
    代理模式 控制对对象的访问 不改变接口,控制访问
    桥接模式 将抽象与实现分离,使它们可以独立变化 分离抽象和实现,而不是转换接口
    外观模式 为复杂的子系统提供一个简单的统一接口 简化接口,而不是转换接口

    一句话总结:

    • 适配器:换个接口
    • 装饰器:加个功能
    • 代理:控个访问
    • 桥接:分个层次
    • 外观:简个调用

    七、现代C++改进建议

    在现代C++(C++11及以后)中,我们可以对适配器模式进行一些改进,让代码更加简洁和安全:

    7.1 使用智能指针管理内存

    如前面的对象适配器例子所示,使用std::unique_ptr或std::shared_ptr来管理适配者对象的生命周期,避免手动内存管理带来的内存泄漏问题。

    7.2 使用函数对象/Lambda简化适配器

    当需要适配的接口只有一个方法时,我们可以使用std::function和Lambda表达式来简化适配器的实现,不需要写一个完整的类:

    #include <functional>

    // 客户端只需要一个函数对象
    using LogFunction = std::function<void(const std::string&, int)>;

    // 直接用Lambda创建适配器
    int main() {
    OldFileLogger oldLogger;

    // 创建适配器函数
    LogFunction logger = [&oldLogger](const std::string& message, int level) {
    std::string formattedMsg = "级别" + std::to_string(level) + ": " + message;
    oldLogger.writeLog(formattedMsg);
    };

    // 客户端调用
    logger("系统启动成功", 1);
    logger("检测到异常连接", 3);

    return 0;
    }

    这种方式非常简洁,适合简单的适配场景。

    7.3 使用模板适配器

    如果需要适配多个具有相同方法签名的类,可以使用模板适配器来避免重复代码:

    template <typename Adaptee>
    class TemplateAdapter : public ILogger {
    private:
    Adaptee adaptee_;

    public:
    TemplateAdapter(Adaptee adaptee) : adaptee_(std::move(adaptee)) {}

    void log(const std::string& message, int level) const override {
    std::string formattedMsg = "级别" + std::to_string(level) + ": " + message;
    adaptee_.writeLog(formattedMsg);
    }
    };

    // 使用
    int main() {
    ILogger* fileLogger = new TemplateAdapter<OldFileLogger>(OldFileLogger());
    fileLogger->log("使用模板适配器", 2);
    delete fileLogger;

    return 0;
    }


    八、总结

    适配器模式是一种非常实用的设计模式,它的核心思想是**“转换接口,兼容不兼容”**。通过引入一个中间适配器类,我们可以在不修改现有代码的情况下,让原本接口不兼容的类能够一起工作。

    在实际开发中,我们应该优先使用对象适配器,因为它的耦合度更低,灵活性更高。只有在需要重写适配者方法的特殊情况下,才考虑使用类适配器。

    记住,设计模式不是银弹。适配器模式虽然强大,但也不能滥用。只有当你确实遇到了接口不兼容的问题,并且修改原有接口会带来很大风险时,才应该使用适配器模式。

    赞(0)
    未经允许不得转载:171主机测评 » 设计模式入门:3. 适配器模式详解 C++实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址