回调与设计模式:策略模式、观察者模式、命令模式中的回调

说实话,很多嵌入式工程师学设计模式时,总觉得这东西是Java、C++那帮搞大型软件的人玩的。我当年也这么想。直到有一次,我在一个智能家居网关项目里,被一堆if-else和状态切换搞得焦头烂额……嗯,从那以后我才明白,设计模式不是花架子,它解决的是代码组织的问题。而回调,恰恰是这些模式在C语言里的“灵魂粘合剂”。

今天咱们就聊聊三个经典模式——策略模式、观察者模式、命令模式。看看回调是怎么在它们里面扮演核心角色的。

1. 策略模式:把“算法”变成回调

策略模式,说白了就是“把不同的做法封装起来,让它们可以互相替换”。你想想看,你的设备可能支持多种通信协议:UART、SPI、I2C。每种协议的收发逻辑都不一样,但你不想在业务代码里写一堆switch-case。

这时候回调就派上用场了。我们把每个协议的“发送”和“接收”函数,注册成回调。业务层只管调用回调,不管底层是谁在干活。

// 定义策略接口(函数指针类型)
typedef void (*send_func_t)(const uint8_t* data, uint32_t len);
typedef void (*recv_func_t)(uint8_t* buf, uint32_t len);

// 策略结构体
typedef struct {
    send_func_t send;
    recv_func_t recv;
} comm_strategy_t;

// 具体策略:UART
void uart_send(const uint8_t* data, uint32_t len) {
    // UART硬件发送逻辑
}
void uart_recv(uint8_t* buf, uint32_t len) {
    // UART硬件接收逻辑
}
comm_strategy_t uart_strategy = { uart_send, uart_recv };

// 具体策略:SPI
void spi_send(const uint8_t* data, uint32_t len) {
    // SPI硬件发送逻辑
}
void spi_recv(uint8_t* buf, uint32_t len) {
    // SPI硬件接收逻辑
}
comm_strategy_t spi_strategy = { spi_send, spi_recv };

// 业务层:只管调用回调
void process_data(comm_strategy_t* strategy, const uint8_t* data, uint32_t len) {
    strategy->send(data, len);
    uint8_t response[64];
    strategy->recv(response, sizeof(response));
    // 处理响应...
}

我的经验: 我在一个多协议传感器采集项目里用过这个模式。当时要支持Modbus RTU(串口)和Modbus TCP(以太网)。如果不用策略模式,业务代码里到处都是if(protocol == UART)……那代码维护起来,简直是一场噩梦。

2. 观察者模式:回调就是“通知”本身

观察者模式,核心就是“一对多”的通知机制。一个事件发生了,多个订阅者要收到通知。在C语言里,这个“通知”就是回调函数。

我习惯用一个简单的“事件总线”来实现。每个观察者注册一个回调,事件发生时,遍历回调列表,挨个调用。

#define MAX_OBSERVERS 10

typedef void (*observer_cb_t)(uint32_t event_id, void* context);

typedef struct {
    observer_cb_t callbacks[MAX_OBSERVERS];
    void* contexts[MAX_OBSERVERS];
    uint8_t count;
} event_bus_t;

void event_bus_init(event_bus_t* bus) {
    bus->count = 0;
}

bool event_bus_register(event_bus_t* bus, observer_cb_t cb, void* ctx) {
    if (bus->count >= MAX_OBSERVERS) return false;
    bus->callbacks[bus->count] = cb;
    bus->contexts[bus->count] = ctx;
    bus->count++;
    return true;
}

void event_bus_notify(event_bus_t* bus, uint32_t event_id) {
    for (uint8_t i = 0; i < bus->count; i++) {
        bus->callbacks[i](event_id, bus->contexts[i]);
    }
}

// 使用示例
void temperature_alarm(uint32_t event_id, void* context) {
    if (event_id == EVENT_TEMP_OVER_LIMIT) {
        // 触发报警
    }
}

event_bus_t bus;
event_bus_init(&bus);
event_bus_register(&bus, temperature_alarm, NULL);

避坑指南: 我曾经在一个实时性要求很高的系统里,让观察者回调里做了大量计算。结果事件通知一多,系统响应就卡顿了。记住:回调里不要做耗时操作。如果必须做,用任务队列把工作丢给后台处理。

3. 命令模式:把“操作”封装成回调

命令模式,就是把一个请求或操作,封装成一个对象。在C语言里,这个“对象”就是一个函数指针加上它的参数。说白了,就是把“你要干什么”和“什么时候干”分离开。

我最常用的场景是“命令队列”。比如一个按键处理模块,按键按下时不是立刻执行操作,而是把操作封装成一个命令,丢到队列里。主循环再从队列里取出来执行。

typedef void (*command_func_t)(void* param);

typedef struct {
    command_func_t func;
    void* param;
} command_t;

#define CMD_QUEUE_SIZE 16

typedef struct {
    command_t buffer[CMD_QUEUE_SIZE];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
} cmd_queue_t;

bool cmd_queue_push(cmd_queue_t* q, command_func_t func, void* param) {
    if (q->count >= CMD_QUEUE_SIZE) return false;
    q->buffer[q->head].func = func;
    q->buffer[q->head].param = param;
    q->head = (q->head + 1) % CMD_QUEUE_SIZE;
    q->count++;
    return true;
}

bool cmd_queue_pop(cmd_queue_t* q, command_t* cmd) {
    if (q->count == 0) return false;
    *cmd = q->buffer[q->tail];
    q->tail = (q->tail + 1) % CMD_QUEUE_SIZE;
    q->count--;
    return true;
}

// 主循环中执行
void main_loop(cmd_queue_t* q) {
    command_t cmd;
    while (cmd_queue_pop(q, &cmd)) {
        cmd.func(cmd.param);
    }
}

核心思想: 命令模式把“调用者”和“执行者”解耦了。调用者只管生成命令(回调+参数),执行者只管按顺序执行。这在多任务系统里特别有用——你可以从一个中断里push命令,在主循环里pop执行。

4. 三种模式的对比

模式 回调的角色 典型场景 我的建议
策略模式 封装算法/行为 多协议通信、多种加密算法 适合“横向替换”的场景
观察者模式 事件通知 按键事件、传感器数据上报 注意回调执行时间,别阻塞
命令模式 封装操作请求 命令队列、撤销/重做 适合“异步执行”或“延迟执行”

5. 一张图看懂:回调与三种设计模式的关系

下面这张SVG图,展示了回调在这三种模式中的核心位置。你看,回调就像一根线,把不同的设计思想串了起来。

回调函数 策略模式 算法可替换 注册策略回调 观察者模式 事件通知 注册通知回调 命令模式 操作封装 封装为命令回调 回调函数是三种设计模式在C语言中的共同实现基础

个人习惯: 我一般在项目初期,先画出“哪些地方需要回调”,然后看这些回调适合套用哪种模式。策略模式适合“替换”,观察者模式适合“通知”,命令模式适合“排队”。选对了模式,代码结构会清晰很多。

好了,今天的内容就到这里。这三种模式,说白了都是“回调”在不同场景下的应用变体。你写嵌入式代码时,如果发现某个模块的if-else越来越多,或者事件处理越来越乱,不妨想想:是不是该用个设计模式来整理一下了?

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