9、回调函数与库设计:设计一个简单的观察者模式库,使用回调解耦

观察者模式,说白了就是「你盯着我,我变了就通知你」。在嵌入式世界里,这种场景太常见了——按键按下要通知上层、传感器数据更新要分发出去、系统状态切换要广播给各个模块。

我早期做项目时,习惯把所有逻辑写在一个大循环里。结果呢?每次加一个新功能,就要改主循环代码。改着改着,自己都看不懂了。后来我意识到,解耦才是嵌入式软件设计的核心。而回调函数,就是实现解耦最锋利的工具。

9.1 观察者模式的核心思想

观察者模式包含两个角色:

  • 主题(Subject):维护一个观察者列表,状态变化时通知所有观察者
  • 观察者(Observer):注册到主题上,收到通知后执行自己的逻辑

用回调来实现,就是主题里存一批函数指针。状态变了,挨个调用这些指针。观察者只需要提供自己的回调函数,完全不用关心主题内部怎么运作。

核心价值:主题和观察者之间没有直接依赖。主题不知道观察者是谁,观察者也不知道主题怎么实现。全靠回调这座「桥」来通信。

9.2 设计一个轻量级观察者库

我习惯把这种库设计成极简风格,不依赖动态内存分配,适合资源受限的MCU。下面是我常用的一个模板:

/* observer.h */
#ifndef OBSERVER_H
#define OBSERVER_H

#include <stdint.h>

/* 事件类型定义 */
typedef enum {
    EVENT_BUTTON_PRESS,
    EVENT_SENSOR_UPDATE,
    EVENT_SYSTEM_ERROR,
    EVENT_MAX
} event_t;

/* 回调函数类型:参数是事件ID和用户自定义数据 */
typedef void (*observer_cb_t)(event_t event, void *arg);

/* 观察者节点 */
typedef struct {
    observer_cb_t callback;
    void *arg;
    struct observer_node *next;
} observer_node_t;

/* 主题结构体 */
typedef struct {
    observer_node_t *head[EVENT_MAX];
} subject_t;

/* 接口函数 */
void subject_init(subject_t *sub);
void subject_register(subject_t *sub, event_t event, 
                      observer_cb_t cb, void *arg);
void subject_unregister(subject_t *sub, event_t event,
                        observer_cb_t cb, void *arg);
void subject_notify(subject_t *sub, event_t event);

#endif /* OBSERVER_H */

嗯,这里要注意:observer_node_t 里的 next 指针我用了单链表。为什么不用数组?因为嵌入式场景下,观察者数量经常动态变化,链表插入删除更灵活。而且我故意把 arg 设计成 void *,这样观察者可以携带任意上下文。

9.3 实现细节与避坑

实现部分其实不复杂,但有几个坑我踩过,必须说一下。

/* observer.c */
#include "observer.h"
#include <stdlib.h>

void subject_init(subject_t *sub) {
    for (int i = 0; i < EVENT_MAX; i++) {
        sub->head[i] = NULL;
    }
}

void subject_register(subject_t *sub, event_t event,
                      observer_cb_t cb, void *arg) {
    if (event >= EVENT_MAX || cb == NULL) return;
    
    /* 防止重复注册 */
    observer_node_t *cur = sub->head[event];
    while (cur) {
        if (cur->callback == cb && cur->arg == arg) return;
        cur = cur->next;
    }
    
    /* 头插法,简单高效 */
    observer_node_t *node = malloc(sizeof(observer_node_t));
    if (node == NULL) return;
    node->callback = cb;
    node->arg = arg;
    node->next = sub->head[event];
    sub->head[event] = node;
}

void subject_notify(subject_t *sub, event_t event) {
    if (event >= EVENT_MAX) return;
    
    observer_node_t *cur = sub->head[event];
    while (cur) {
        cur->callback(event, cur->arg);
        cur = cur->next;
    }
}

我曾经踩过的坑:在回调函数里又去注册或注销观察者,导致链表被修改,遍历指针变成野指针。解决方案有两种:一是用「延迟操作」队列,把注册注销请求攒到通知结束后再处理;二是用双缓冲链表。我个人偏向第一种,实现简单,不容易出bug。

9.4 使用示例:按键事件分发

来看看实际怎么用。假设我们有一个按键模块,按下时触发事件:

/* main.c */
#include "observer.h"
#include <stdio.h>

/* 两个观察者:一个记录日志,一个控制LED */
void log_callback(event_t event, void *arg) {
    printf("按键按下,时间戳: %lu\n", *(uint32_t*)arg);
}

void led_callback(event_t event, void *arg) {
    /* 假设arg指向LED状态 */
    uint8_t *state = (uint8_t*)arg;
    *state = !(*state);
    printf("LED状态切换为: %d\n", *state);
}

int main() {
    subject_t subject;
    subject_init(&subject);
    
    uint32_t timestamp = 0;
    uint8_t led_state = 0;
    
    /* 注册两个观察者 */
    subject_register(&subject, EVENT_BUTTON_PRESS, 
                     log_callback, &timestamp);
    subject_register(&subject, EVENT_BUTTON_PRESS,
                     led_callback, &led_state);
    
    /* 模拟按键按下 */
    while (1) {
        /* 假设这里检测到按键 */
        timestamp = get_system_tick();
        subject_notify(&subject, EVENT_BUTTON_PRESS);
        
        /* 延时防抖 */
        delay_ms(100);
    }
    
    return 0;
}

你看,主循环里只需要调用 subject_notify,具体谁处理、怎么处理,完全由注册的回调决定。想加一个新功能?写个回调函数,注册上去就行。主循环一行代码都不用改。

9.5 观察者模式库的架构图

下面这张图展示了整个库的核心流程。我画的时候特意把「主题」和「观察者」分在两侧,中间用回调函数连接,这样解耦关系一目了然。

观察者模式库架构图 主题 (Subject) 维护事件链表 register / unregister notify 事件链表 (单链表) 节点1: callback + arg 节点2: callback + arg 节点3: ... 观察者 (Observer) 提供回调函数 处理事件 携带上下文 (arg) 观察者实例 日志记录器 LED控制器 蜂鸣器驱动 回调函数调用 主题通过回调函数通知所有注册的观察者,实现完全解耦

9.6 进阶:静态分配版本

有些场景下不能用 malloc,比如在中断里注册观察者,或者系统对实时性要求极高。这时候可以用静态数组代替链表:

/* 静态分配版本 */
#define MAX_OBSERVERS_PER_EVENT 8

typedef struct {
    observer_cb_t callbacks[MAX_OBSERVERS_PER_EVENT];
    void *args[MAX_OBSERVERS_PER_EVENT];
    uint8_t count[EVENT_MAX];
} subject_static_t;

void subject_static_register(subject_static_t *sub, 
                             event_t event,
                             observer_cb_t cb, 
                             void *arg) {
    if (event >= EVENT_MAX || cb == NULL) return;
    if (sub->count[event] >= MAX_OBSERVERS_PER_EVENT) return;
    
    uint8_t idx = sub->count[event]++;
    sub->callbacks[idx] = cb;
    sub->args[idx] = arg;
}

我的建议:如果观察者数量固定且不多(比如不超过8个),用静态数组。代码更简单,没有内存碎片问题。如果观察者数量动态变化,或者需要频繁注册注销,用链表。我个人在量产项目中,80%的情况用静态数组就够了。

9.7 回调机制在库设计中的价值

你想想看,如果没有回调,库和用户代码之间怎么通信?要么库提供注册函数让用户轮询,要么库直接调用用户定义的全局函数。前者增加CPU负担,后者导致强耦合。

回调机制让库设计者可以写出「不知道用户是谁,但知道怎么通知用户」的代码。这就是解耦的精髓。我参与过一个传感器融合库的设计,底层驱动用观察者模式上报原始数据,上层算法模块注册回调接收处理后的结果。整个架构清晰得像搭积木。

方案 耦合度 灵活性 内存开销 适用场景
全局函数调用 极低 简单裸机程序
轮询查询 状态机驱动
回调函数(观察者) 极低 模块化设计、RTOS
消息队列 极低 极高 多任务、复杂系统

从表格能看出来,回调方案在灵活性和开销之间取得了很好的平衡。这也是为什么它在嵌入式领域如此流行。

好了,关于观察者模式库的设计就聊到这里。记住一句话:回调是解耦的钥匙,观察者是回调的舞台。下次你写库的时候,不妨试试这个模式,你会发现代码的可维护性提升一个档次。