适配器模式:接口适配、数据结构适配、协议转换

说实话,适配器模式是我在嵌入式开发中用得最多的模式之一。为什么?因为嵌入式系统里,你永远在和各种外设、芯片、协议打交道。这些家伙各有各的脾气,接口千奇百怪。你不可能让所有东西都按你的规矩来——那怎么办?加个适配器,让它们互相理解。

说白了,适配器模式就是做「翻译」的。它把A的接口转成B能理解的接口。两个模块本来没法直接对话,中间加一层适配,问题就解决了。

核心思想:将一个类的接口转换成客户希望的另一个接口。适配器模式让原本接口不兼容的类可以合作无间。

三种适配场景

我在实际项目中,适配器模式主要用在三个地方:

  • 接口适配——函数参数、返回值、调用方式不匹配
  • 数据结构适配——数据格式、字节序、对齐方式不同
  • 协议转换——通信协议、消息格式需要翻译

你想想看,这三种情况是不是每天都在嵌入式开发里出现?

接口适配:让函数握手

我记得有一次做传感器驱动。原厂给的驱动库,初始化函数是 sensor_init(int i2c_addr),但我项目里统一用的是 device_open(uint8_t bus_id, uint8_t dev_addr)。直接调用?不行。改原厂库?不现实。加个适配层就完了。

// 原厂接口
int sensor_init(int i2c_addr);

// 项目统一接口
int device_open(uint8_t bus_id, uint8_t dev_addr);

// 适配器
int device_open_adapter(uint8_t bus_id, uint8_t dev_addr) {
    // 忽略bus_id,直接调用原厂接口
    return sensor_init((int)dev_addr);
}

就这么简单。但你别小看这层适配。它让上层代码完全不用关心底层用的是哪家的芯片。换传感器?改适配器就行,上层纹丝不动。

我的习惯:适配层单独放一个文件,比如 hw_adapter.c。所有硬件相关的适配都集中在这里,方便维护,也方便移植。

数据结构适配:字节序与对齐

这个坑我踩过。有一次做STM32和DSP之间的SPI通信。两边都是32位处理器,但字节序不一样。STM32是小端,DSP是大端。直接传结构体?数据全乱套。

// 发送端结构体(小端)
typedef struct {
    uint16_t id;
    uint32_t value;
    uint8_t  status;
} SensorData;

// 接收端结构体(大端)
typedef struct {
    uint16_t id;
    uint32_t value;
    uint8_t  status;
} __attribute__((packed)) SensorDataBigEndian;

// 适配函数:小端转大端
void sensor_data_to_big_endian(const SensorData *src, SensorDataBigEndian *dst) {
    dst->id     = __builtin_bswap16(src->id);
    dst->value  = __builtin_bswap32(src->value);
    dst->status = src->status;  // 单字节不用转
}

嗯,这里要注意。结构体对齐也是个坑。两个处理器如果对齐方式不同,直接memcpy过去,字段位置全错。适配器里必须做逐字段的拷贝和转换,不能偷懒。

我曾经犯过的错:以为两个ARM处理器之间传结构体没问题。结果一个用4字节对齐,另一个用1字节对齐。排查了两天才发现是对齐问题。从那以后,跨芯片通信我必加适配层。

协议转换:让不同协议对话

协议转换是适配器模式的重头戏。我做过一个项目,主控用Modbus RTU,但新加的一个传感器只支持I2C。怎么办?写一个协议适配器。

// Modbus请求结构
typedef struct {
    uint8_t  slave_addr;
    uint8_t  func_code;
    uint16_t reg_addr;
    uint16_t reg_count;
    uint16_t crc;
} ModbusRequest;

// I2C命令结构
typedef struct {
    uint8_t  dev_addr;
    uint8_t  cmd;
    uint8_t  data_len;
    uint8_t  data[8];
} I2CCommand;

// 协议适配器
int modbus_to_i2c_adapter(const ModbusRequest *modbus_req, I2CCommand *i2c_cmd) {
    // 地址映射
    i2c_cmd->dev_addr = modbus_slave_to_i2c_addr(modbus_req->slave_addr);
    
    // 功能码转换
    switch (modbus_req->func_code) {
        case 0x03:  // 读保持寄存器
            i2c_cmd->cmd = I2C_CMD_READ;
            break;
        case 0x06:  // 写单个寄存器
            i2c_cmd->cmd = I2C_CMD_WRITE;
            break;
        default:
            return -1;  // 不支持的协议
    }
    
    // 数据长度转换
    i2c_cmd->data_len = modbus_req->reg_count * 2;
    
    return 0;
}

你看,适配器把Modbus的请求翻译成了I2C能理解的命令。上层应用只发Modbus指令,底层实际走的是I2C。中间发生了什么?上层完全不知道,也不需要知道。

适配器模式的两种实现方式

在C语言里,适配器模式通常有两种实现方式:

方式 说明 适用场景
函数适配 写一个包装函数,内部调用原接口 接口参数不匹配、返回值需要转换
结构体适配 定义新的结构体,包含原结构体指针,提供转换方法 数据结构复杂、需要状态管理

我个人更倾向于函数适配。简单、直接、开销小。结构体适配虽然灵活,但增加了内存开销和复杂度。嵌入式环境里,能简单就别复杂。

一张图看懂适配器模式

下面这张图展示了适配器模式的核心逻辑。左边是客户代码,右边是被适配的模块,中间就是适配器。它负责把客户的请求转成被适配模块能理解的形式。

适配器模式结构图 客户代码 调用 Target 接口 适配器 实现 Target 接口 持有 Adaptee 引用 被适配模块 原有接口 不兼容的协议/格式 调用 转换后调用 适配器的工作流程: 1. 客户代码调用适配器的 Target 接口 2. 适配器将请求转换成被适配模块能理解的形式 3. 被适配模块执行实际工作,返回结果 4. 适配器将结果转换回客户代码期望的格式 接口适配 · 数据结构适配 · 协议转换

实际项目中的避坑指南

适配器模式看着简单,但用不好也会翻车。我总结几个经验:

  • 不要过度适配。如果两个模块的接口差异太大,强行加适配器反而让代码更乱。这时候该重构就重构。
  • 适配层要有错误处理。协议转换过程中,可能遇到不支持的指令、超时、数据校验失败。适配器必须把这些错误清晰地返回给上层。
  • 性能开销要心里有数。每层适配都有代价。高频调用的路径上,适配器要尽量轻量。我一般用内联函数或者宏来减少调用开销。
  • 文档要写清楚。适配器做了哪些转换,为什么这么转,不写清楚的话,三个月后你自己都看不懂。

一个小技巧:适配器里加个调试开关。调试时打印转换前后的数据,排查问题特别方便。发布时关掉就行。

总结

适配器模式不是什么高深的技术。它就是一层「翻译」,让不兼容的模块能合作。嵌入式开发里,你几乎每天都在用——只是可能没意识到而已。

接口不匹配?加适配器。字节序不对?加适配器。协议不同?加适配器。记住一点:适配器是为了解耦,不是为了增加复杂度。如果加了适配器反而让系统更乱,那一定是设计出了问题。

嗯,适配器模式就讲到这里。代码写得多了,你自然就会形成自己的适配风格。


公众号:蓝海资料掘金营,微信deep3321