C++设计模式:适配器模式(Adapter)

1. 概述

GoF分类

适配器模式属于结构型设计模式

官方定义

将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。

核心目的

解决两个已有接口之间不匹配的问题,通过创建一个中间适配层来实现接口转换。

主要问题

当我们需要使用一个现有的类,但其接口与我们需要的接口不兼容时:

  • 第三方库接口与我们的系统不匹配

  • 新旧系统接口不一致

  • 需要统一多个类似功能的接口

C++典型应用价值

  • 集成遗留代码到新系统

  • 封装第三方库接口

  • 统一不同厂商的设备驱动接口

  • 为测试创建mock对象

关键特征

  • 通过组合或继承实现接口转换

  • 不修改原有接口

  • 保持单一职责原则

  • 透明性:客户端不知道适配器的存在

2. 核心思想

设计哲学

“不要修改别人提供的接口,而是创建一个适配器来转换接口”

面向对象原则

  • 开闭原则​:不修改原有类,通过扩展来适配

  • 单一职责原则​:适配器只负责接口转换

  • 依赖倒置原则​:依赖抽象接口而非具体实现

组件关系

  • Target​:客户端期望的接口

  • Adaptee​:需要被适配的现有接口

  • Adapter​:实现Target接口并包装Adaptee

C++特殊考虑

  • 资源管理:适配器可能需要管理Adaptee的生命周期

  • 异常安全:确保适配操作不会泄漏资源

  • 性能:避免不必要的间接调用

工作流程示意图

Client -> Target Interface
           ↑
       Adapter (implements Target)
           ↓
       Adaptee (original interface)

3. 适用场景

应该使用适配器模式的场景

场景1:集成第三方库

// 我们的系统接口
class DataProcessor {
public:
    virtual std::string process(const std::string&) = 0;
};

// 第三方库接口
class ThirdPartyLib {
public:
    char* transformData(const char* input);
};

// 适配器
class LibAdapter : public DataProcessor {
    ThirdPartyLib lib;
public:
    std::string process(const std::string& input) override {
        auto result = lib.transformData(input.c_str());
        return std::string(result);
    }
};

场景2:设备驱动统一接口

// 标准设备接口
class Device {
public:
    virtual void turnOn() = 0;
};

// 老式设备
class LegacyDevice {
public:
    void powerUp() { /*...*/ }
};

class DeviceAdapter : public Device {
    LegacyDevice device;
public:
    void turnOn() override { device.powerUp(); }
};

场景3:新旧系统兼容

// 新系统接口
class NewSystem {
public:
    virtual void execute(const std::vector<int>&) = 0;
};

// 旧系统接口
class OldSystem {
public:
    void run(int*, size_t);
};

class SystemAdapter : public NewSystem {
    OldSystem system;
public:
    void execute(const std::vector<int>& data) override {
        system.run(const_cast<int*>(data.data()), data.size());
    }
};

不适合使用适配器模式的场景

场景1:接口差异过大

  • 问题:需要适配的接口功能完全不同

  • 解决方案:考虑重构或使用外观模式

场景2:频繁变化的接口

  • 问题:被适配接口经常变化

  • 解决方案:直接修改客户端代码或使用策略模式

场景3:性能关键路径

  • 问题:适配器引入额外开销

  • 解决方案:直接修改接口或使用模板特化

4. UML类图

5. 结构

模式参与者

Target(目标接口)​

  • 角色:定义客户端使用的领域特定接口

  • 职责:声明客户端期望的操作

Adaptee(被适配者)​

  • 角色:需要被适配的现有接口

  • 职责:提供实际的功能实现

Adapter(适配器)​

  • 角色:接口转换器

  • 职责:

    • 实现Target接口

    • 维护对Adaptee对象的引用

    • 将Target接口调用转发给Adaptee

Client(客户端)​

  • 角色:与符合Target接口的对象协作

  • 职责:通过Target接口调用适配后的功能

协作流程

  1. 客户端调用适配器的方法

  2. 适配器将请求转换为Adaptee的一个或多个调用

  3. 客户端接收到结果,但不知道适配器的存在

6. 代码示例

// 目标接口:现代C++接口
class ModernSocket {
public:
    virtual ~ModernSocket() = default;
    virtual void connect(const std::string& host, uint16_t port) = 0;
    virtual void send(const std::vector<uint8_t>& data) = 0;
    virtual std::vector<uint8_t> receive() = 0;
};

// 被适配者:遗留C风格接口
class LegacySocket {
    int sockfd = -1;
public:
    void open_connection(const char* host, int port) {
        // 实际实现省略...
        sockfd = /* socket操作 */;
    }
    
    int write_data(const void* buf, size_t len) {
        return ::send(sockfd, buf, len, 0);
    }
    
    int read_data(void* buf, size_t len) {
        return ::recv(sockfd, buf, len, 0);
    }
    
    void close_connection() {
        if (sockfd != -1) {
            ::close(sockfd);
            sockfd = -1;
        }
    }
};

// 对象适配器(推荐方式)
class SocketAdapter : public ModernSocket {
    std::unique_ptr<LegacySocket> socket;
public:
    explicit SocketAdapter(std::unique_ptr<LegacySocket> sock)
        : socket(std::move(sock)) {}
    
    ~SocketAdapter() override = default;
    
    void connect(const std::string& host, uint16_t port) override {
        socket->open_connection(host.c_str(), static_cast<int>(port));
    }
    
    void send(const std::vector<uint8_t>& data) override {
        if (socket->write_data(data.data(), data.size()) < 0) {
            throw std::runtime_error("Send failed");
        }
    }
    
    std::vector<uint8_t> receive() override {
        std::vector<uint8_t> buffer(1024);
        auto len = socket->read_data(buffer.data(), buffer.size());
        if (len < 0) {
            throw std::runtime_error("Receive failed");
        }
        buffer.resize(len);
        return buffer;
    }
};

// 类适配器(通过多重继承)
class SocketClassAdapter : public ModernSocket, private LegacySocket {
public:
    void connect(const std::string& host, uint16_t port) override {
        open_connection(host.c_str(), static_cast<int>(port));
    }
    
    void send(const std::vector<uint8_t>& data) override {
        if (write_data(data.data(), data.size()) < 0) {
            throw std::runtime_error("Send failed");
        }
    }
    
    std::vector<uint8_t> receive() override {
        std::vector<uint8_t> buffer(1024);
        auto len = read_data(buffer.data(), buffer.size());
        if (len < 0) {
            throw std::runtime_error("Receive failed");
        }
        buffer.resize(len);
        return buffer;
    }
};

// 客户端代码
void clientCode(ModernSocket& socket) {
    socket.connect("example.com", 8080);
    socket.send({'h', 'e', 'l', 'l', 'o'});
    auto response = socket.receive();
    std::cout << "Received " << response.size() << " bytes\n";
}

int main() {
    // 使用对象适配器
    {
        auto adapter = std::make_unique<SocketAdapter>(
            std::make_unique<LegacySocket>());
        clientCode(*adapter);
    }
    
    // 使用类适配器
    {
        SocketClassAdapter adapter;
        clientCode(adapter);
    }
    
    return 0;
}

7. 运行结果

Connected to example.com:8080
Sent 5 bytes
Received 128 bytes
Connected to example.com:8080
Sent 5 bytes
Received 128 bytes

8. 应用场景

实际项目中的应用案例

C++标准库

  • std::stackstd::queue作为底层容器的适配器

  • 迭代器适配器(reverse_iterator, move_iterator)

Boost库

  • boost::adaptors(范围适配器)

  • boost::asio中的协议适配器

游戏开发

  • 输入设备适配器(统一键盘/手柄/触摸输入)

  • 图形API适配器(OpenGL/Vulkan/DirectX抽象层)

系统编程

  • 文件系统适配器(统一不同OS的文件操作)

  • 网络协议适配器(TCP/UDP统一接口)

9. 变体

主要变体实现

双向适配器

class TwoWayAdapter : public ModernSocket, public LegacySocket {
    // 实现两个方向的接口转换
};

模板适配器

template <typename Adaptee>
class GenericAdapter : public ModernSocket {
    Adaptee adaptee;
    // 通用适配实现
};

延迟适配器

class LazyAdapter : public ModernSocket {
    std::function<std::unique_ptr<LegacySocket>()> factory;
    std::unique_ptr<LegacySocket> socket;
public:
    explicit LazyAdapter(std::function<std::unique_ptr<LegacySocket>()> f)
        : factory(std::move(f)) {}
    
    void ensureInitialized() {
        if (!socket) {
            socket = factory();
        }
    }
    // 其他方法委托给socket...
};

变体对比表格

变体名称优点缺点适用场景
对象适配器灵活,运行时绑定需要额外间接层大多数通用场景
类适配器高效,编译时绑定多重继承复杂性简单适配场景
模板适配器类型安全,高效编译时代码膨胀需要适配多种类型
双向适配器双向转换接口复杂需要互操作的系统

10. 适配器模式 vs. 其他模式

与外观模式的区别

  • 适配器​:解决接口不匹配问题,转换一个接口

  • 外观​:简化复杂子系统,提供统一的高层接口

与装饰器模式的区别

  • 适配器​:改变对象接口

  • 装饰器​:增强对象功能而不改变接口

选择指南

  • 当需要解决接口不兼容时选择适配器

  • 当需要简化复杂系统时选择外观

  • 当需要动态添加功能时选择装饰器

11. 适配器模式的优缺点

优点分析

  1. 接口兼容性
  • 允许不相关的类一起工作

  • 在C++中特别有用,因为C++需要与C和其他语言交互

  1. 开闭原则
  • 无需修改现有代码即可引入新接口

  • 对测试友好,可以创建测试适配器

  1. 复用性
  • 可以复用大量已有代码

  • 特别适合集成遗留系统

缺点分析

  1. 复杂性增加
  • 引入额外间接层

  • 在C++中需要注意资源管理和异常安全

  1. 过度使用风险
  • 可能导致系统结构复杂化

  • 可能隐藏设计问题而非真正解决

12. 什么时候不要使用适配器模式?

明确的反对信号

  • 接口差异过大​:当需要适配的接口功能完全不同时

  • 可以直接修改​:当你有权修改接口且修改更简单时

  • 性能关键路径​:当适配器会成为性能瓶颈时

反模式警示

  • 适配器链​:多个适配器串联导致难以维护

  • 隐藏问题​:用适配器掩盖设计缺陷而非解决问题

  • 接口污染​:适配器暴露了不需要的方法

13. 总结

核心价值总结

适配器模式是集成不兼容接口的强大工具,特别适合C++这种需要与多种语言和系统交互的环境。

最佳实践建议

  • 优先使用对象适配器而非类适配器

  • 确保适配器是异常安全的

  • 考虑使用智能指针管理资源

  • 为适配器编写单元测试

进阶学习路径

  • 研究C++标准库中的适配器实现

  • 学习模板元编程实现编译期适配

  • 探索双向适配器和多接口适配

  • 实践集成真实世界的API(如数据库驱动)

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐