观察者模式:设计事件驱动接口,实现一对多的依赖关系解耦
各位同学,今天我们来聊聊观察者模式。说实话,这个模式在嵌入式系统里太常用了。你想想看,一个按键按下,要通知好几个模块——LED要闪、蜂鸣器要响、日志要记录。如果把这些逻辑全写死在按键检测函数里,那代码就成了一团乱麻。
观察者模式,说白了就是解决「一对多通知」的问题。一个事件源,多个监听者。事件源不需要知道监听者是谁,监听者也不需要关心事件源怎么工作的。这就是解耦。
从实际需求说起
我在项目中遇到过这样一个场景:一个温度传感器,每秒钟采集一次数据。数据来了之后,需要做三件事:更新显示、判断是否超温报警、记录到历史缓冲区。最开始,我直接在采集函数里依次调用这三个函数。结果呢?
- 想加一个新功能(比如上传到云端),得改采集函数
- 想关掉某个功能(比如暂时不报警),还得改采集函数
- 测试的时候,每个功能都耦合在一起,没法单独测
这就是典型的紧耦合。观察者模式就是来解决这个问题的。
核心设计思路
观察者模式的核心就两个角色:主题(Subject)和观察者(Observer)。主题负责维护一个观察者列表,当事件发生时,遍历列表通知所有观察者。观察者则实现一个统一的接口,接收通知并做出响应。
用C语言实现,我习惯用函数指针。每个观察者就是一个回调函数,主题里存一个函数指针数组。嗯,这里要注意:数组大小要提前规划好,或者用动态链表。
关键点:观察者模式的核心是「订阅-发布」机制。观察者先向主题注册,主题才知道要通知谁。注册和注销接口是必须的。
代码实现:一个轻量级的事件驱动框架
下面我给出一个完整的实现。这个框架我用了好几年,在多个项目里验证过,稳定可靠。
/* event_driver.h */
#ifndef EVENT_DRIVER_H
#define EVENT_DRIVER_H
#include <stdint.h>
/* 事件类型定义 */
typedef enum {
EVENT_TEMPERATURE_UPDATE,
EVENT_BUTTON_PRESS,
EVENT_ALARM_TRIGGER,
EVENT_MAX
} event_type_t;
/* 观察者回调函数类型 */
typedef void (*observer_callback_t)(void *data, uint32_t data_len);
/* 主题结构体 */
typedef struct {
observer_callback_t callbacks[EVENT_MAX][10]; /* 每个事件最多10个观察者 */
uint8_t count[EVENT_MAX]; /* 每个事件的观察者数量 */
} subject_t;
/* 接口函数 */
void subject_init(subject_t *sub);
int subject_register(subject_t *sub, event_type_t event, observer_callback_t cb);
int subject_unregister(subject_t *sub, event_type_t event, observer_callback_t cb);
void subject_notify(subject_t *sub, event_type_t event, void *data, uint32_t data_len);
#endif
/* event_driver.c */
#include "event_driver.h"
#include <string.h>
void subject_init(subject_t *sub) {
memset(sub, 0, sizeof(subject_t));
}
int subject_register(subject_t *sub, event_type_t event, observer_callback_t cb) {
if (event >= EVENT_MAX) return -1;
if (sub->count[event] >= 10) return -2; /* 观察者已满 */
/* 检查是否已注册 */
for (int i = 0; i < sub->count[event]; i++) {
if (sub->callbacks[event][i] == cb) {
return -3; /* 重复注册 */
}
}
sub->callbacks[event][sub->count[event]++] = cb;
return 0;
}
int subject_unregister(subject_t *sub, event_type_t event, observer_callback_t cb) {
if (event >= EVENT_MAX) return -1;
for (int i = 0; i < sub->count[event]; i++) {
if (sub->callbacks[event][i] == cb) {
/* 用最后一个覆盖当前,然后减一 */
sub->callbacks[event][i] = sub->callbacks[event][sub->count[event] - 1];
sub->count[event]--;
return 0;
}
}
return -2; /* 未找到 */
}
void subject_notify(subject_t *sub, event_type_t event, void *data, uint32_t data_len) {
if (event >= EVENT_MAX) return;
for (int i = 0; i < sub->count[event]; i++) {
if (sub->callbacks[event][i] != NULL) {
sub->callbacks[event][i](data, data_len);
}
}
}
使用示例
来看看怎么用这个框架。假设我们有温度传感器、显示模块、报警模块。
/* 观察者回调函数 */
void display_update(void *data, uint32_t len) {
float *temp = (float *)data;
/* 更新显示逻辑 */
printf("Display: %.2f°C\n", *temp);
}
void alarm_check(void *data, uint32_t len) {
float *temp = (float *)data;
if (*temp > 50.0f) {
printf("ALARM: Temperature too high!\n");
}
}
/* 主程序 */
subject_t g_subject;
void temperature_sensor_task(void) {
float current_temp = read_sensor();
/* 通知所有观察者 */
subject_notify(&g_subject, EVENT_TEMPERATURE_UPDATE,
¤t_temp, sizeof(current_temp));
}
int main(void) {
subject_init(&g_subject);
/* 注册观察者 */
subject_register(&g_subject, EVENT_TEMPERATURE_UPDATE, display_update);
subject_register(&g_subject, EVENT_TEMPERATURE_UPDATE, alarm_check);
while (1) {
temperature_sensor_task();
delay(1000);
}
}
我的经验:观察者回调函数里不要做耗时操作。如果某个观察者需要处理大量数据,建议用消息队列把任务丢给后台处理。否则一个观察者卡住了,其他观察者都得等着。
架构图:观察者模式的核心流程
下面这张图展示了观察者模式的完整工作流程。从注册到通知,一目了然。
避坑指南
我曾经在一个项目里踩过一个大坑。当时观察者回调函数里直接调用了free()释放了事件数据的内存。结果另一个观察者还在用这个数据,直接野指针崩溃。调试了一整天才找到原因。
所以,关于观察者模式,有几点经验分享:
- 数据所有权要明确:通知时传递的数据,是主题分配还是观察者分配?谁负责释放?必须在设计文档里写清楚。
- 不要在通知循环里注册/注销:如果在某个观察者的回调里调用了register或unregister,会破坏正在遍历的数组。我一般用延迟处理,或者加一个「待处理列表」。
- 考虑优先级:有些观察者需要优先响应(比如紧急停止),有些可以延后。可以在注册时传入优先级参数,通知时按优先级排序。
- 注意中断上下文:如果事件来自中断,观察者回调里不能做阻塞操作。我习惯在中断里只设置标志位,在主循环里调用notify。
警告:观察者模式虽然好用,但不要滥用。如果只有一两个观察者,直接函数调用更简单。过度设计也是罪。我见过有人把两个函数之间的调用也改成观察者模式,结果代码绕来绕去,维护成本反而高了。
性能考量
观察者模式会引入一定的性能开销。主要是函数指针调用和循环遍历。在资源受限的MCU上,需要注意:
| 场景 | 建议 | 原因 |
|---|---|---|
| 观察者数量 < 5 | 直接使用数组,线性遍历 | 代码简单,性能足够 |
| 观察者数量 > 10 | 考虑用链表或哈希表 | 减少内存浪费,方便动态管理 |
| 事件频率 > 1kHz | 用位图标记,批量通知 | 减少函数调用次数 |
| 中断中触发 | 只设置标志,主循环处理 | 避免在中断里做复杂操作 |
总结
观察者模式是嵌入式事件驱动架构的基石。它让模块之间的依赖关系从「硬编码」变成了「可配置」。你想想看,用了我上面这个框架,以后加一个新功能,只需要写一个回调函数,然后register一下就行。原来的代码一行都不用改。
这就是解耦的魅力。也是我们做嵌入式系统架构追求的目标——高内聚、低耦合。
好了,今天的分享就到这里。代码我已经在多个项目里验证过,你可以直接拿去用。如果有问题,欢迎交流。