嵌入式C++轻量级观察者模式库设计与实践

张开发
2026/4/13 1:04:58 15 分钟阅读

分享文章

嵌入式C++轻量级观察者模式库设计与实践
1. 项目概述ESPressio-Observable 是一个专为微控制器环境设计的轻量级、面向对象的观察者模式Observer Pattern实现库。它并非简单的回调函数封装而是构建在严格 SOLID 原则之上的、可扩展的事件通知基础设施。该库是 Flowduino ESPressio 开发平台的核心组件之一其设计哲学直指嵌入式开发中最棘手的耦合问题如何在资源受限的 MCU 上实现松散耦合、高内聚、可维护性强的模块化架构。在裸机或 RTOS 环境中传统状态轮询Polling或单一回调Callback机制极易导致代码僵化。当一个传感器模块需要同时通知日志器、显示器和网络上传模块时硬编码的回调链会迅速演变为难以维护的“意大利面条式”代码。ESPressio-Observable 提供了一种范式级的解决方案它将“谁来通知”与“谁来响应”彻底解耦使 Observable被观察者仅需关注自身状态变化而 IObserver观察者则通过声明式接口Interface自主决定响应哪些事件。这种设计不仅消除了循环引用Circular Reference的风险更将系统复杂度从 O(n²) 的网状依赖降维至 O(n) 的星型结构。1.1 核心设计哲学与工程目标ESPressio-Observable 的每一个 API 和数据结构都服务于三个不可妥协的工程目标极致轻量Light-weight所有容器均基于静态数组或预分配内存池实现杜绝运行时动态内存分配malloc/free避免堆碎片和不确定的执行时间。核心Observable类的实例仅占用约 24 字节不含用户数据IObserverHandle仅 8 字节。零侵入式抽象Ease of Use不强制要求用户继承特定基类除IObserver外不引入宏魔法或代码生成。所有功能通过标准 C 模板和虚函数实现与 Arduino、PlatformIO、Zephyr 等主流开发框架无缝集成。SOLID 原则的嵌入式实践在 C 语言限制下最大程度践行 SOLID单一职责SRPObservable只负责管理观察者列表与分发通知IObserver只定义通知契约IObserverHandle只负责生命周期管理。开闭原则OCP通过模板参数TInterface扩展通知类型无需修改Observable源码。里氏替换LSPThreadSafeObservable完全兼容Observable接口可无感替换。接口隔离ISP每个业务场景定义专属接口如ITemperatureObserver避免“胖接口”污染。依赖倒置DIPObservable仅依赖IObserver抽象不依赖任何具体观察者实现。1.2 与通用观察者模式的本质区别市面上多数 MCU 观察者库如 Arduino 的PubSubClient或某些 HAL 封装通常采用以下两种模式之一单接口硬编码模式所有观察者必须实现一个固定接口如void onEvent(int data)导致业务逻辑混杂无法区分温度变化与压力变化。函数指针数组模式使用std::functionvoid()或函数指针数组存储回调虽灵活但引入虚函数表开销或无法捕获上下文this指针。ESPressio-Observable 采用第三条路径——接口映射Interface-Mapped模式。其核心创新在于WithObserversTInterface()模板方法它能从全局观察者列表中精准筛选出所有实现了指定接口TInterface的观察者并仅向它们分发通知。这意味着一个Thermometer实例可以同时拥有ITemperatureObserver、IAirPressureObserver和IBatteryObserver三类观察者且彼此完全隔离。这种能力直接源于 C RTTIRun-Time Type Information的支持也是该库要求启用 RTTI 的根本原因。2. 核心组件与 API 详解ESPressio-Observable 的命名空间为ESPressio::Observable所有类型均在此空间下定义。其核心组件构成一个清晰的契约-实现-管理三层结构。2.1 核心接口Contracts接口名称作用关键方法工程意义IObserver观察者身份标识virtual ~IObserver() default;所有观察者类必须公有继承此接口作为类型系统的根。析构函数为虚确保delete observerHandle能正确调用派生类析构。IObservable被观察者行为契约virtual IObserverHandle* RegisterObserver(IObserver observer) 0;virtual void UnregisterObserver(IObserverHandle* handle) 0;定义了注册/注销观察者的标准协议。Observable和ThreadSafeObservable均实现此接口保证上层代码可互换。IObserverHandle观察者生命周期句柄virtual void Unregister() 0;由RegisterObserver()返回是观察者在Observable内部的唯一身份凭证。调用Unregister()即从通知列表中移除该观察者。2.2 核心实现类ImplementationsObservable非线程安全版这是最基础、性能最高的实现适用于单线程裸机环境如 STM32 HAL FreeRTOS 单任务。#include ESPressio_Observable.hpp using namespace ESPressio::Observable; class Thermometer : public Observable::Observable { private: int _temperature; // 注意_temperature 的读写必须在单线程上下文中完成否则需额外同步 public: // 关键NotifyObservers 是纯虚函数必须由子类实现 void NotifyObservers(int oldTemp, int newTemp) override { // 1. 使用 WithObserversT 精准筛选 ITemperatureObserver WithObserversITemperatureObserver([oldTemp, newTemp](ITemperatureObserver* obs) { // 2. 对每个匹配的观察者调用其接口方法 obs-OnTemperatureChanged(oldTemp, newTemp); if (newTemp oldTemp) { obs-OnTemperatureIncreased(newTemp - oldTemp); } else if (oldTemp newTemp) { obs-OnTemperatureDecreased(oldTemp - newTemp); } }); } void UpdateTemperature() { int newTemp read_sensor(); // 伪代码读取实际传感器值 if (_temperature newTemp) return; // 无变化不通知 NotifyObservers(_temperature, newTemp); // 触发通知 _temperature newTemp; // 更新内部状态 } };Observable::Observable的内部实现极为精简维护一个std::arrayIObserver*, MAX_OBSERVERS静态数组MAX_OBSERVERS默认为 16可编译时配置。RegisterObserver()将IObserver的地址存入数组空位并返回一个指向新创建的ObserverHandle的指针。WithObserversT()通过dynamic_castT*(observer)对数组中每个元素进行类型检查仅对成功转换的指针执行 Lambda。ThreadSafeObservable线程安全版当Observable运行于多任务环境如 FreeRTOS 多个任务并发调用UpdateTemperature()时必须使用此版本。#include ESPressio_ThreadSafeObservable.hpp // 替换继承关系即可其余代码完全不变 class Thermometer : public ThreadSafeObservable { // ... 其余代码同上 };ThreadSafeObservable的实现差异仅在于在RegisterObserver()、UnregisterObserver()和WithObserversT()的关键临界区自动调用xSemaphoreTake()/xSemaphoreGive()FreeRTOS或portENTER_CRITICAL()/portEXIT_CRITICAL()裸机。重要警告线程安全仅保障观察者列表的增删查操作不保障Observable子类的成员变量如_temperature。若UpdateTemperature()和GetTemperature()可能被不同任务调用则_temperature必须用独立的互斥量保护。ObserverHandle句柄实现ObserverHandle是IObserverHandle的具体实现其构造函数接收IObserver*和IObservable*的弱引用并在析构时自动调用UnregisterObserver()。// 用户代码中应始终保存此指针 IObserverHandle* handle thermometer.RegisterObserver(logger); // 当需要取消订阅时 delete handle; // 此操作等价于: handle-Unregister(); delete handle;2.3 关键模板方法WithObserversTInterface这是整个库的“引擎”其签名如下templatetypename TInterface void WithObservers(std::functionvoid(TInterface*) callback) const;TInterface用户自定义的纯虚接口如ITemperatureObserver必须公有继承IObserver。callback一个 Lambda 或函数对象接收TInterface*参数即已成功类型转换的观察者指针。工作流程遍历内部IObserver*数组。对每个IObserver* observer执行dynamic_castTInterface*(observer)。若转换成功返回非空指针则调用callback(converted_ptr)。若转换失败返回nullptr则跳过该观察者。此机制完美支持“一个 Observable多种 Observer”的场景且无任何运行时开销dynamic_cast在单继承下是常数时间。3. 工程实践从零构建一个传感器监控系统本节将基于 ESPressio-Observable构建一个完整的、生产就绪的传感器监控系统涵盖温度、气压、电池三类数据源并演示其在 FreeRTOS 多任务环境下的应用。3.1 定义领域专用接口ISP 原则的体现首先为每个业务域定义最小、最专注的接口。接口中的方法均提供空实现{}而非纯虚 0这允许观察者只重写关心的方法。// ITemperatureObserver.hpp #pragma once #include ESPressio_IObserver.hpp class ITemperatureObserver : public ESPressio::Observable::IObserver { public: virtual void OnTemperatureChanged(int oldTemp, int newTemp) {} virtual void OnTemperatureThresholdExceeded(int currentTemp, int threshold) {} }; // IAirPressureObserver.hpp #pragma once #include ESPressio_IObserver.hpp class IAirPressureObserver : public ESPressio::Observable::IObserver { public: virtual void OnAirPressureChanged(int oldPress, int newPress) {} virtual void OnAirPressureTrendStable() {} // 气压稳定趋势 }; // IBatteryObserver.hpp #pragma once #include ESPressio_IObserver.hpp class IBatteryObserver : public ESPressio::Observable::IObserver { public: virtual void OnBatteryLevelChanged(float oldPercent, float newPercent) {} virtual void OnBatteryStateChange(bool isCharging) {} };3.2 实现可复用的 Observable 基类SRP 原则为避免重复代码创建一个泛型传感器基类SensorBase它封装了状态变更检测与通知的通用逻辑。// SensorBase.hpp #pragma once #include ESPressio_ThreadSafeObservable.hpp #include functional templatetypename TDataType class SensorBase : public ESPressio::Observable::ThreadSafeObservable { protected: TDataType _currentValue; std::functionTDataType() _readFunction; // 传感器读取函数 bool _isInitialized; // 模板化的通知方法由子类调用 templatetypename TInterface, typename... Args void NotifyInterface(Args... args) { WithObserversTInterface([args...](TInterface* obs) { obs-OnDataChanged(std::forwardArgs(args)...); }); } public: explicit SensorBase(std::functionTDataType() readFunc) : _readFunction(readFunc), _isInitialized(false), _currentValue{} {} void Initialize() { _currentValue _readFunction(); _isInitialized true; } void Update() { if (!_isInitialized) return; TDataType newValue _readFunction(); if (newValue ! _currentValue) { // 通知所有实现了 IGenericSensorObserver 的观察者 NotifyInterfaceIGenericSensorObserver(_currentValue, newValue); _currentValue newValue; } } TDataType GetValue() const { return _currentValue; } }; // 为通用传感器定义一个基础接口 class IGenericSensorObserver : public ESPressio::Observable::IObserver { public: virtual void OnDataChanged(const auto oldValue, const auto newValue) 0; };3.3 构建具体传感器 ObservableOCP 原则现在Thermometer的实现变得极其简洁只需关注业务逻辑。// Thermometer.hpp #pragma once #include SensorBase.hpp #include ITemperatureObserver.hpp class Thermometer : public SensorBaseint { public: using SensorBase::SensorBase; void UpdateTemperature() { Update(); // 复用基类逻辑 } // 重写 NotifyObservers 以提供温度特有通知 void NotifyObservers(int oldTemp, int newTemp) override { // 1. 通知通用接口 NotifyInterfaceIGenericSensorObserver(oldTemp, newTemp); // 2. 通知温度专用接口 WithObserversITemperatureObserver([oldTemp, newTemp](ITemperatureObserver* obs) { obs-OnTemperatureChanged(oldTemp, newTemp); if (newTemp 35) { // 简单阈值 obs-OnTemperatureThresholdExceeded(newTemp, 35); } }); } };3.4 实现多角色观察者LSP 原则一个DisplayManager同时监听三种数据源完美展示一个观察者实现多个接口的能力。// DisplayManager.hpp #pragma once #include ESPressio_IObserver.hpp #include ITemperatureObserver.hpp #include IAirPressureObserver.hpp #include IBatteryObserver.hpp #include SSD1306.h // 假设的 OLED 驱动 class DisplayManager : public ESPressio::Observable::IObserver, public ITemperatureObserver, public IAirPressureObserver, public IBatteryObserver { private: SSD1306 _display; int _temp 0, _press 0; float _battery 0.0f; bool _charging false; void RefreshScreen() { _display.clear(); _display.drawString(0, 0, Temp: String(_temp) C); _display.drawString(0, 16, Press: String(_press) hPa); _display.drawString(0, 32, Bat: String(_battery, 1) %); _display.drawString(0, 48, _charging ? CHARGING : DISCHARGING); _display.display(); } public: DisplayManager(SSD1306 display) : _display(display) {} // ITemperatureObserver 实现 void OnTemperatureChanged(int oldTemp, int newTemp) override { _temp newTemp; RefreshScreen(); } // IAirPressureObserver 实现 void OnAirPressureChanged(int oldPress, int newPress) override { _press newPress; RefreshScreen(); } // IBatteryObserver 实现 void OnBatteryLevelChanged(float oldPercent, float newPercent) override { _battery newPercent; RefreshScreen(); } void OnBatteryStateChange(bool isCharging) override { _charging isCharging; RefreshScreen(); } };3.5 FreeRTOS 多任务集成生产环境部署在main.cpp中我们将传感器更新、网络上报等任务分离到不同 RTOS 任务中ThreadSafeObservable确保了跨任务通知的安全性。// main.cpp #include Arduino.h #include freertos/FreeRTOS.h #include freertos/task.h #include Thermometer.hpp #include DisplayManager.hpp #include NetworkUploader.hpp // 假设的网络上传观察者 Thermometer thermometer([](){ return analogRead(A0) * 0.1; }); // 简单模拟 DisplayManager displayManager(ssd1306); NetworkUploader uploader; void task_sensor_update(void* pvParameters) { for(;;) { thermometer.UpdateTemperature(); vTaskDelay(1000 / portTICK_PERIOD_MS); // 每秒更新一次 } } void task_display_update(void* pvParameters) { for(;;) { // DisplayManager 已在各 OnXXX 方法中自动刷新屏幕 vTaskDelay(100 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); ssd1306.init(); // 注册所有观察者 thermometer.RegisterObserver(displayManager); thermometer.RegisterObserver(uploader); // 创建 RTOS 任务 xTaskCreate(task_sensor_update, SENSOR, 2048, NULL, 1, NULL); xTaskCreate(task_display_update, DISPLAY, 2048, NULL, 1, NULL); } void loop() { // FreeRTOS 调度器接管loop 不再执行 }4. 高级主题与最佳实践4.1 内存管理与生命周期控制在 MCU 上对象生命周期管理是核心挑战。ESPressio-Observable 提供了两种模式静态对象推荐所有Observable和IObserver派生类均声明为全局或static变量。IObserverHandle*也应为静态变量确保其生命周期覆盖整个程序运行期。动态对象谨慎使用若必须new一个观察者务必在销毁前显式delete其IObserverHandle。// 错误handle 未被删除观察者仍注册在 Observable 中 auto* logger new TemperatureLogger(); thermometer.RegisterObserver(*logger); // 正确获取 handle 并在适当时候销毁 IObserverHandle* handle thermometer.RegisterObserver(*logger); // ... 使用一段时间后 delete handle; // 自动从 thermometer 中注销 delete logger;4.2 性能剖析与优化时间复杂度WithObserversT()的时间复杂度为 O(N)其中 N 是注册的总观察者数。对于大多数 MCU 应用N 32这是可接受的。若 N 极大可考虑按接口类型分组维护多个数组。空间复杂度每个IObserverHandle占用 8 字节两个指针。Observable的数组大小由ESPRESSIO_OBSERVABLE_MAX_OBSERVERS宏定义默认 16。可通过platformio.ini覆盖build_flags -DESPRESSIO_OBSERVABLE_MAX_OBSERVERS64RTTI 开销dynamic_cast在 ARM Cortex-M 上通常编译为几条指令远低于一次printf或 SPI 传输的开销。启用 RTTI 是值得的权衡。4.3 与 ESPressio Event 库的协同ESPressio-Observable 是“事件驱动”的基石而ESPressio-Event库则是其上层的“事件总线”。当系统规模扩大Observable之间的直接依赖变得复杂时应升级为事件总线模式// 不再让 Thermometer 直接 NotifyObservers // 而是发布一个事件 EventBus::PublishTemperatureChangeEvent(oldTemp, newTemp); // DisplayManager 订阅该事件 EventBus::SubscribeTemperatureChangeEvent([](const TemperatureChangeEvent e) { displayManager.OnTemperatureChanged(e.oldTemp, e.newTemp); });这实现了真正的“发布-订阅”Pub-Sub是Observable模式的终极演进。5. PlatformIO 集成与构建配置在platformio.ini中必须进行以下关键配置以确保 RTTI 正常工作尤其是在 ESP32 平台上。[env:esp32dev] platform https://github.com/platformio/platform-espressif32.git board esp32dev framework arduino ; 必须使用支持 RTTI 的 ESP32 Arduino 核心 platform_packages framework-arduinoespressif32 https://github.com/espressif/arduino-esp32.git ; 显式启用 RTTI build_unflags -fno-rtti build_flags -fexceptions -frtti ; 添加 ESPressio-Observable 库 lib_deps flowduino/ESPressio-Observable^1.0.1 ; 或使用最新开发版 ; https://github.com/Flowduino/ESPressio-Observable.git若项目中使用了#include FS.h上述platform_packages配置是必需的以规避旧版核心中 RTTI 与 FS 库的链接冲突。此问题已在 ESP32 Arduino 核心 3.0.0 版本中修复。6. 故障排除与常见陷阱陷阱1未启用 RTTI编译错误error: dynamic_cast not permitted with -fno-rtti。解决检查build_unflags是否包含-fno-rtti并确认build_flags包含-frtti。陷阱2观察者未被通知原因IObserver派生类未公有继承或RegisterObserver()的参数不是IObserver的引用。解决检查继承语法class MyObs : public IObserver并确认调用observable.RegisterObserver(myObs)传入对象本身非指针。陷阱3线程安全假象现象ThreadSafeObservable未防止_temperature的竞态。解决ThreadSafeObservable仅保护其内部列表。对Observable子类的成员变量必须使用SemaphoreHandle_t或StaticSemaphore_t进行独立保护。陷阱4Lambda 捕获导致栈溢出现象在WithObservers的 Lambda 中捕获大型对象如std::string可能导致栈溢出。解决只捕获基本类型或指针或使用std::shared_ptr管理堆上对象。一个Thermometer实例在初始化后其内部的IObserver*数组便已确定。当UpdateTemperature()被调用NotifyObservers()会遍历该数组对每个元素执行dynamic_castITemperatureObserver*。只有那些真正实现了ITemperatureObserver的观察者才会被选中并执行其OnTemperatureChanged方法。这个过程不涉及任何虚函数表的遍历也不需要反射纯粹是 C 标准所保证的、高效的类型安全转换。

更多文章