行为型模式:观察者模式

做DSP开发或嵌入式编程的同仁,大概率都碰到过这类场景:中断触发后,得同步完成显示更新、日志记录、后续控制逻辑触发;状态机切换状态时,多个模块要根据新状态执行对应操作。要是直接把这些逻辑硬编码到中断服务函数或状态切换函数里,后续新增功能就得反复修改核心代码,不仅容易引入bug,维护起来也格外棘手。

其实,解决这类"一个事件触发多个后续动作"的问题,有个经典且实用的设计模式------观察者模式。今天,我就按"原理拆解→工程化分析→C语言实现→实战验证→问题解决"的逻辑,手把手带大家吃透这个模式,帮你写出更具扩展性、可维护性的嵌入式代码。

一、原理拆解:观察者模式的核心逻辑

观察者模式的核心思想很直白:定义对象间的一对多依赖关系,当被观察对象(我们称之为"主题")的状态发生变化时,会自动通知所有依赖它的对象(我们称之为"观察者"),并驱动观察者执行相应的更新操作。

用通俗的话讲,这就像我们订阅报纸:你(观察者)向报社(主题)订阅报纸,一旦报社有新报纸出版(状态变化),就会主动把报纸送到所有订阅者手上,你完全不用每天跑报社询问。这里的关键是"主动通知"和"解耦"------订阅者和报社之间没有强绑定,订阅者可随时取消订阅,报社也不用关心订阅者拿到报纸后具体怎么处理。

从结构上看,观察者模式主要包含两个核心角色:

  1. 主题(Subject):作为被观察的核心对象,核心职责有三个:维护一个观察者链表,提供观察者的注册(订阅)、注销(取消订阅)接口,以及在自身状态变化时,遍历观察者链表并通知所有观察者执行更新操作。

  2. 观察者(Observer):作为依赖主题的对象,必须定义一个统一的更新接口。当收到主题的通知时,通过这个接口执行具体的业务逻辑。不同观察者可实现不同的更新逻辑,从而实现功能的差异化扩展。

通过这两个角色的配合,观察者模式实现了"主题"与"观察者"的解耦:主题不用知道具体有哪些观察者,也不用关心观察者的具体实现;观察者只需通过统一接口注册到主题,就能被动接收通知。这种解耦特性让两者可以独立迭代更新,大幅提升代码的扩展性。

二、工程化分析:为什么嵌入式开发需要观察者模式?

可能有朋友会疑惑:嵌入式开发讲究轻量、高效,直接用函数回调不也能实现类似功能吗?为啥非要用观察者模式?其实在工程化落地场景中,观察者模式相比直接写回调,有三个不可替代的优势:

  1. 解耦核心逻辑与扩展逻辑:以中断事件处理为例,中断触发是核心逻辑,日志记录、显示更新、控制逻辑属于扩展逻辑。用观察者模式后,中断服务函数只需负责通知观察者,无需关心具体的扩展逻辑;后续新增扩展功能时,只需新增观察者并注册,不用修改中断核心代码,完全符合"开闭原则"。

  2. 统一管理依赖关系:如果直接用多个回调函数,需要手动维护回调函数指针数组,注册、注销逻辑容易混乱。观察者模式通过主题统一管理观察者链表,接口清晰规范,能有效避免分散管理带来的bug。

  3. 支持动态扩展与灵活配置:嵌入式设备在不同场景下可能需要不同的功能组合,比如调试模式下需要日志记录,正常运行模式下则不需要。借助观察者模式,可动态注册/注销观察者,实现功能的灵活切换,无需重新编译代码。

当然,观察者模式也不是万能的。在嵌入式场景中使用时,有两个问题需要重点关注:一是线程安全问题,尤其是在多线程或中断上下文通知观察者时,容易出现链表操作冲突;二是性能问题,若观察者过多或更新逻辑复杂,会导致通知耗时过长,影响系统实时性,同时还要避免主题与观察者之间出现循环依赖。这些问题,我们后面会给出具体的解决方案。

三、C语言实现:手把手写一个基础版本

C语言没有类和对象的概念,但我们可以用结构体模拟"主题"和"观察者",用函数指针实现统一的更新接口。下面,我们就一步步实现一个基础版本的观察者模式,帮大家理解核心实现逻辑。

3.1 定义核心结构体与接口

首先定义观察者结构体,核心是一个更新函数指针,用于让不同观察者实现差异化的更新逻辑;接着定义主题结构体,包含观察者链表、链表当前长度、最大容量,以及注册、注销、通知三大核心接口的函数指针。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 前向声明主题结构体,观察者的更新函数需要用到
typedef struct Subject Subject;

// 观察者结构体:核心是统一的更新接口(函数指针)
typedef struct Observer {
    // 更新函数:参数为主题指针(可获取主题状态)和自定义数据
    void (*update)(Subject* subject, void* data);
    // 用于区分不同观察者,方便注销
    int id;
} Observer;

// 主题结构体:维护观察者链表,提供注册/注销/通知接口
typedef struct Subject {
    // 观察者链表(动态数组,适应灵活增减)
    Observer** observers;
    // 当前观察者数量
    int observer_count;
    // 链表最大容量(避免频繁扩容,提升性能)
    int max_observers;
    // 主题状态(示例:用整数表示,实际可根据需求定义)
    int state;
    
    // 接口函数指针
    // 注册观察者
    int (*register_observer)(Subject* subject, Observer* observer);
    // 注销观察者
    int (*unregister_observer)(Subject* subject, int observer_id);
    // 通知所有观察者
    void (*notify_observers)(Subject* subject, void* data);
} Subject;

3.2 实现主题的核心接口

接下来实现主题的注册、注销、通知这三个核心函数。注册函数负责将观察者添加到链表中,注销函数负责移除指定ID的观察者,通知函数负责遍历观察者链表,调用每个观察者的更新函数。

c 复制代码
// 注册观察者
static int subject_register_observer(Subject* subject, Observer* observer) {
    if (subject == NULL || observer == NULL) {
        printf("Error: subject or observer is NULL\n");
        return -1;
    }
    
    // 检查观察者是否已注册(避免重复注册)
    for (int i = 0; i < subject->observer_count; i++) {
        if (subject->observers[i]->id == observer->id) {
            printf("Warning: observer %d already registered\n", observer->id);
            return 0;
        }
    }
    
    // 链表满了,扩容(这里简单扩容为原来的2倍,可根据需求调整)
    if (subject->observer_count >= subject->max_observers) {
        int new_max = subject->max_observers * 2;
        Observer** new_observers = realloc(subject->observers, new_max * sizeof(Observer*));
        if (new_observers == NULL) {
            printf("Error: realloc failed\n");
            return -1;
        }
        subject->observers = new_observers;
        subject->max_observers = new_max;
        printf("Info: subject observers list expanded to %d\n", new_max);
    }
    
    // 添加观察者到链表
    subject->observers[subject->observer_count++] = observer;
    printf("Info: observer %d registered successfully\n", observer->id);
    return 0;
}

// 注销观察者
static int subject_unregister_observer(Subject* subject, int observer_id) {
    if (subject == NULL) {
        printf("Error: subject is NULL\n");
        return -1;
    }
    
    // 查找观察者在链表中的位置
    int index = -1;
    for (int i = 0; i < subject->observer_count; i++) {
        if (subject->observers[i]->id == observer_id) {
            index = i;
            break;
        }
    }
    
    if (index == -1) {
        printf("Warning: observer %d not found\n", observer_id);
        return -1;
    }
    
    // 移除观察者(将后面的元素向前移动一位)
    for (int i = index; i < subject->observer_count - 1; i++) {
        subject->observers[i] = subject->observers[i + 1];
    }
    subject->observer_count--;
    printf("Info: observer %d unregistered successfully\n", observer_id);
    return 0;
}

// 通知所有观察者
static void subject_notify_observers(Subject* subject, void* data) {
    if (subject == NULL) {
        printf("Error: subject is NULL\n");
        return;
    }
    
    // 遍历观察者链表,调用每个观察者的更新函数
    for (int i = 0; i < subject->observer_count; i++) {
        if (subject->observers[i]->update != NULL) {
            subject->observers[i]->update(subject, data);
        }
    }
}

// 初始化主题
Subject* subject_init(int max_observers) {
    if (max_observers <= 0) {
        printf("Error: max_observers must be positive\n");
        return NULL;
    }
    
    Subject* subject = malloc(sizeof(Subject));
    if (subject == NULL) {
        printf("Error: malloc subject failed\n");
        return NULL;
    }
    
    // 初始化观察者链表
    subject->observers = malloc(max_observers * sizeof(Observer*));
    if (subject->observers == NULL) {
        printf("Error: malloc observers list failed\n");
        free(subject);
        return NULL;
    }
    
    subject->observer_count = 0;
    subject->max_observers = max_observers;
    subject->state = 0; // 初始状态为0
    
    // 绑定接口函数
    subject->register_observer = subject_register_observer;
    subject->unregister_observer = subject_unregister_observer;
    subject->notify_observers = subject_notify_observers;
    
    printf("Info: subject initialized successfully\n");
    return subject;
}

// 销毁主题(释放资源)
void subject_destroy(Subject* subject) {
    if (subject == NULL) {
        return;
    }
    
    if (subject->observers != NULL) {
        free(subject->observers);
    }
    free(subject);
    printf("Info: subject destroyed successfully\n");
}

3.3 实现观察者的更新逻辑

观察者需要实现具体的更新函数,不同观察者可根据业务需求实现不同的更新逻辑。这里我们以嵌入式开发中常见的"中断事件通知"场景为例,实现两个典型观察者:一个负责记录中断日志,一个负责更新显示界面。

c 复制代码
// 观察者1:记录日志
void log_observer_update(Subject* subject, void* data) {
    if (subject == NULL || data == NULL) {
        return;
    }
    int event_type = *(int*)data;
    printf("Log Observer: Interrupt event %d triggered, subject state: %d\n", event_type, subject->state);
}

// 观察者2:更新显示
void display_observer_update(Subject* subject, void* data) {
    if (subject == NULL || data == NULL) {
        return;
    }
    int event_type = *(int*)data;
    printf("Display Observer: Show interrupt event %d, current state: %d\n", event_type, subject->state);
}

// 初始化观察者
Observer* observer_init(int id, void (*update)(Subject* subject, void* data)) {
    if (update == NULL) {
        printf("Error: update function is NULL\n");
        return NULL;
    }
    
    Observer* observer = malloc(sizeof(Observer));
    if (observer == NULL) {
        printf("Error: malloc observer failed\n");
        return NULL;
    }
    
    observer->id = id;
    observer->update = update;
    return observer;
}

// 销毁观察者
void observer_destroy(Observer* observer) {
    if (observer != NULL) {
        free(observer);
    }
}

四、实战验证:中断事件通知场景测试

下面我们结合"中断事件触发"的实战场景,测试基础版本观察者模式的功能。假设外部中断触发时(主题状态发生变化),需要通知日志观察者记录事件、显示观察者更新界面,同时支持动态注销观察者。

c 复制代码
int main() {
    // 1. 初始化主题(最大支持4个观察者)
    Subject* subject = subject_init(4);
    if (subject == NULL) {
        return -1;
    }
    
    // 2. 初始化两个观察者
    Observer* log_observer = observer_init(1, log_observer_update);
    Observer* display_observer = observer_init(2, display_observer_update);
    if (log_observer == NULL || display_observer == NULL) {
        observer_destroy(log_observer);
        observer_destroy(display_observer);
        subject_destroy(subject);
        return -1;
    }
    
    // 3. 注册观察者
    subject->register_observer(subject, log_observer);
    subject->register_observer(subject, display_observer);
    
    // 4. 模拟中断事件1触发(主题状态更新为1,通知观察者)
    printf("\n--- Interrupt Event 1 Triggered ---\n");
    subject->state = 1;
    int event1 = 1;
    subject->notify_observers(subject, &event1);
    
    // 5. 注销显示观察者
    subject->unregister_observer(subject, 2);
    
    // 6. 模拟中断事件2触发(主题状态更新为2,通知观察者)
    printf("\n--- Interrupt Event 2 Triggered ---\n");
    subject->state = 2;
    int event2 = 2;
    subject->notify_observers(subject, &event2);
    
    // 7. 释放资源
    observer_destroy(log_observer);
    observer_destroy(display_observer);
    subject_destroy(subject);
    
    return 0;
}

4.1 测试结果与分析

编译运行上述代码,输出结果如下。通过结果我们可以直观验证观察者模式的核心功能是否正常:

text 复制代码
Info: subject initialized successfully
Info: observer 1 registered successfully
Info: observer 2 registered successfully

--- Interrupt Event 1 Triggered ---
Log Observer: Interrupt event 1 triggered, subject state: 1
Display Observer: Show interrupt event 1, current state: 1

Info: observer 2 unregistered successfully

--- Interrupt Event 2 Triggered ---
Log Observer: Interrupt event 2 triggered, subject state: 2
Info: subject destroyed successfully

从输出结果可以清晰看出:

  1. 两个观察者成功注册后,中断事件1触发时,两者都收到了主题的通知,并执行了各自的更新逻辑(记录日志、更新显示);

  2. 注销显示观察者后,中断事件2触发时,只有日志观察者收到通知并执行操作,实现了观察者的动态增减;

  3. 主题与观察者之间完全解耦,若后续需要新增"中断事件控制"功能,只需新增一个控制观察者并实现update函数,注册到主题即可,无需修改主题的核心代码。

五、问题解决:线程安全与性能优化

上面实现的基础版本,在单线程、非中断场景下可以正常使用。但在嵌入式实际开发中,多线程或中断上下文通知观察者的场景很常见,这时候就必须解决线程安全和性能优化问题,否则会导致程序运行异常。

5.1 线程安全实现

线程安全的核心问题是"并发修改观察者链表"------比如一个线程正在注册观察者(修改链表),另一个线程同时在通知观察者(遍历链表),很容易导致链表访问越界、数据错乱等问题。在嵌入式开发中,解决这个问题最常用的两种方式是:用互斥锁保护链表操作(针对多线程场景)、关闭中断保护(针对中断上下文场景)。

下面我们基于互斥锁优化主题的接口函数(以FreeRTOS的互斥锁为例,其他操作系统可替换为对应的锁机制,核心逻辑一致):

c 复制代码
#include "FreeRTOS.h"
#include "semphr.h"

// 扩展主题结构体,添加互斥锁
typedef struct Subject {
    Observer** observers;
    int observer_count;
    int max_observers;
    int state;
    SemaphoreHandle_t mutex; // 互斥锁
    
    int (*register_observer)(Subject* subject, Observer* observer);
    int (*unregister_observer)(Subject* subject, int observer_id);
    void (*notify_observers)(Subject* subject, void* data);
} Subject;

// 初始化主题(添加互斥锁初始化)
Subject* subject_init(int max_observers) {
    // 省略前面的初始化代码...
    
    // 创建互斥锁
    subject->mutex = xSemaphoreCreateMutex();
    if (subject->mutex == NULL) {
        printf("Error: create mutex failed\n");
        free(subject->observers);
        free(subject);
        return NULL;
    }
    
    return subject;
}

// 优化注册函数,添加锁保护
static int subject_register_observer(Subject* subject, Observer* observer) {
    if (subject == NULL || observer == NULL) {
        return -1;
    }
    
    // 获取互斥锁(等待时间100ms,可根据需求调整)
    if (xSemaphoreTake(subject->mutex, pdMS_TO_TICKS(100)) != pdPASS) {
        printf("Error: take mutex failed for register\n");
        return -1;
    }
    
    // 省略中间的注册逻辑...
    
    // 释放互斥锁
    xSemaphoreGive(subject->mutex);
    return 0;
}

// 同理优化注销和通知函数,在访问链表前后加锁/解锁
static int subject_unregister_observer(Subject* subject, int observer_id) {
    if (subject == NULL) {
        return -1;
    }
    
    if (xSemaphoreTake(subject->mutex, pdMS_TO_TICKS(100)) != pdPASS) {
        printf("Error: take mutex failed for unregister\n");
        return -1;
    }
    
    // 省略中间的注销逻辑...
    
    xSemaphoreGive(subject->mutex);
    return 0;
}

static void subject_notify_observers(Subject* subject, void* data) {
    if (subject == NULL) {
        return;
    }
    
    if (xSemaphoreTake(subject->mutex, pdMS_TO_TICKS(100)) != pdPASS) {
        printf("Error: take mutex failed for notify\n");
        return;
    }
    
    // 省略遍历通知逻辑...
    
    xSemaphoreGive(subject->mutex);
}

如果观察者模式需要在中断上下文使用,由于中断上下文不能使用阻塞式的互斥锁,此时建议采用"关闭中断保护"的方式,确保链表操作的原子性:

c 复制代码
// 中断上下文的通知函数
static void subject_notify_observers_isr(Subject* subject, void* data) {
    if (subject == NULL) {
        return;
    }
    
    // 保存中断状态,关闭中断
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
    
    // 遍历观察者链表,通知观察者
    for (int i = 0; i < subject->observer_count; i++) {
        if (subject->observers[i]->update != NULL) {
            subject->observers[i]->update(subject, data);
        }
    }
    
    // 恢复中断状态
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}

5.2 性能优化与循环依赖避免

在嵌入式实时系统中,观察者模式的性能优化核心是"保障实时性",同时要避免循环依赖导致的程序死循环。主要从以下两个方面入手:

  • 控制观察者数量:根据实际业务需求设定合理的max_observers,避免观察者链表过长导致遍历耗时增加,影响系统实时性;

  • 简化update函数逻辑:观察者的update函数要尽量简洁,避免在其中执行复杂计算、大量IO等耗时操作;若确实需要执行耗时操作,可通过消息队列将任务抛给后台线程处理,避免阻塞主题的通知流程;

  • 使用静态链表替代动态链表:若观察者数量固定(如嵌入式设备的固定功能模块),可直接用静态数组实现观察者链表,避免malloc/realloc带来的性能开销和内存碎片问题。

  1. 减少通知耗时,保障实时性

    • 添加通知标记:在主题中增加"当前通知观察者ID"标记,通知过程中若再次触发主题状态变化,跳过当前正在通知的观察者;

    • 明确职责边界:严格划分主题和观察者的职责,主题只负责状态管理和通知,观察者仅负责接收通知并执行自身逻辑,不允许观察者主动修改主题的状态。

  2. 避免循环依赖,防止死循环:循环依赖是指观察者A的update函数中,会触发主题的状态变化,而主题状态变化后又会再次通知观察者A,最终导致无限循环。解决方式主要有两种:

六、总结

总结一下,观察者模式通过"主题-观察者"的一对多依赖关系,实现了核心逻辑与扩展逻辑的解耦,非常适配嵌入式开发中"一个事件触发多个后续动作"的场景,比如中断事件通知、状态机状态变更回调、消息订阅/发布系统等。

今天我们从原理拆解、工程化价值分析,到手把手实现C语言基础版本、结合中断场景实战验证,最后解决了线程安全和性能优化这两个核心工程问题,相信大家已经掌握了观察者模式在嵌入式开发中的核心用法。实际项目中,大家可根据具体场景灵活调整实现细节,比如用静态链表替代动态链表、适配不同OS的锁机制等。

如果这篇文章对你的嵌入式开发工作有帮助,别忘了点赞、收藏,关注我!后续会持续分享更多嵌入式开发相关的设计模式实战、性能优化技巧、问题排查方法。如果在使用观察者模式的过程中遇到了具体问题,或者有其他想深入了解的技术点,欢迎在评论区留言讨论!

相关推荐
小程同学>o<2 小时前
嵌入式之C/C++(二)内存
c语言·开发语言·c++·笔记·嵌入式软件·面试题库
浅念-2 小时前
C语言——内存函数
c语言·经验分享·笔记·学习·算法
水饺编程2 小时前
第4章,[标签 Win32] :系统字体与字符大小
c语言·c++·windows·visual studio
LYS_06182 小时前
寒假学习(8)(c语言8+模数电8)
c语言·学习·pcb
心态还需努力呀4 小时前
【鸿蒙 PC 命令行适配】c-ares 在鸿蒙 PC 上的移植与交叉编译实战(可复现指南)
c语言·开源·harmonyos·鸿蒙·openharmony
代码无bug抓狂人4 小时前
(蓝桥杯省B)R格式
c语言·蓝桥杯
养军博客4 小时前
C语言五天速成(可用于蓝桥杯备考)
c语言·数据结构·算法
Yupureki4 小时前
《算法竞赛从入门到国奖》算法基础:搜索-BFS初识
c语言·数据结构·c++·算法·visual studio·宽度优先
叫我辉哥e113 小时前
### 技术文章大纲:C语言造轮子大赛
c语言·开发语言