35、设计模式(上):单例模式、工厂模式、观察者模式的C语言实现
说到设计模式,很多嵌入式工程师第一反应是:「那是Java、C++才玩的东西吧?」
其实不然。我在嵌入式领域摸爬滚打了十几年,发现真正优秀的C代码,背后都有设计模式的影子。只不过它们没有「类」和「继承」这些语法糖,实现起来更考验功底。
今天我们就聊聊三种最常用的模式:单例、工厂、观察者。我会结合C语言的特性,给出真正能在单片机上跑起来的实现。
一、单例模式:全局唯一的「老大」
单例模式,说白了就是保证一个类只有一个实例。在C语言里,我们通常用它来管理硬件外设——比如UART、SPI、系统时钟。
你想想看,一个系统里如果有两个UART实例同时操作同一个串口,那不乱套了?
经典实现:懒汉式
// uart_manager.h
typedef struct {
int baudrate;
int data_bits;
void (*send)(const char* data);
char* (*receive)(void);
} UartManager;
UartManager* uart_manager_get_instance(void);
// uart_manager.c
static UartManager* instance = NULL;
UartManager* uart_manager_get_instance(void) {
if (instance == NULL) {
// 注意:这里需要加锁,如果是RTOS环境
instance = (UartManager*)malloc(sizeof(UartManager));
if (instance) {
instance->baudrate = 115200;
instance->data_bits = 8;
instance->send = uart_send_impl;
instance->receive = uart_receive_impl;
}
}
return instance;
}
static UartManager instance = { .baudrate = 115200 };
避坑指南
我曾经在一个多任务项目里,两个任务同时调用get_instance(),结果instance被创建了两次。嗯,这就是典型的竞态条件。
解决方案很简单:
- RTOS环境:加互斥锁
- 裸机环境:在初始化阶段就创建好,后续只读
二、工厂模式:对象的「流水线」
工厂模式解决的是「创建对象」的问题。当你的系统需要根据配置或运行时状态,动态创建不同类型的对象时,工厂模式就派上用场了。
我记得有一次做传感器采集系统,需要支持温度、湿度、气压三种传感器。如果直接在业务代码里写if-else,那代码会变得又臭又长。
简单工厂实现
// sensor.h
typedef struct {
int (*init)(void);
float (*read)(void);
void (*deinit)(void);
} Sensor;
typedef enum {
SENSOR_TEMPERATURE,
SENSOR_HUMIDITY,
SENSOR_PRESSURE
} SensorType;
Sensor* sensor_create(SensorType type);
// sensor_factory.c
Sensor* sensor_create(SensorType type) {
Sensor* sensor = NULL;
switch (type) {
case SENSOR_TEMPERATURE:
sensor = (Sensor*)malloc(sizeof(Sensor));
if (sensor) {
sensor->init = temp_sensor_init;
sensor->read = temp_sensor_read;
sensor->deinit = temp_sensor_deinit;
}
break;
case SENSOR_HUMIDITY:
// 类似实现...
break;
case SENSOR_PRESSURE:
// 类似实现...
break;
default:
return NULL;
}
return sensor;
}
工厂模式的变体
在实际项目中,我更喜欢用「注册式工厂」。每个传感器驱动自己注册到工厂里,工厂维护一个函数指针表。这样新增传感器时,不需要修改工厂代码——符合开闭原则。
// 注册表结构
typedef struct {
SensorType type;
Sensor* (*create)(void);
} SensorRegistry;
static SensorRegistry registry[MAX_SENSORS];
static int registry_count = 0;
int sensor_register(SensorType type, Sensor* (*create_fn)(void)) {
if (registry_count >= MAX_SENSORS) return -1;
registry[registry_count].type = type;
registry[registry_count].create = create_fn;
registry_count++;
return 0;
}
Sensor* sensor_create(SensorType type) {
for (int i = 0; i < registry_count; i++) {
if (registry[i].type == type) {
return registry[i].create();
}
}
return NULL;
}
三、观察者模式:事件驱动的「广播站」
观察者模式,说白了就是「发布-订阅」。一个对象状态变化时,所有依赖它的对象都会收到通知。
在嵌入式系统里,这个模式太常用了。比如按键检测、数据采集完成、错误报警……
C语言实现
// observer.h
typedef void (*EventHandler)(void* context);
typedef struct {
EventHandler handler;
void* context;
} Observer;
typedef struct {
Observer observers[MAX_OBSERVERS];
int count;
} Subject;
void subject_init(Subject* sub) {
sub->count = 0;
}
int subject_attach(Subject* sub, EventHandler handler, void* context) {
if (sub->count >= MAX_OBSERVERS) return -1;
sub->observers[sub->count].handler = handler;
sub->observers[sub->count].context = context;
sub->count++;
return 0;
}
void subject_notify(Subject* sub) {
for (int i = 0; i < sub->count; i++) {
sub->observers[i].handler(sub->observers[i].context);
}
}
// 按键检测模块
Subject key_subject;
void key_isr_handler(void) {
// 按键按下,通知所有观察者
subject_notify(&key_subject);
}
void led_on(void* context) {
// 点亮LED
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
void buzzer_beep(void* context) {
// 蜂鸣器响一声
HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);
HAL_Delay(100);
HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);
}
// 初始化
subject_init(&key_subject);
subject_attach(&key_subject, led_on, NULL);
subject_attach(&key_subject, buzzer_beep, NULL);
观察者模式的注意事项
我曾经在一个中断服务函数里直接调用subject_notify,结果观察者函数里调用了printf——嗯,死机了。中断里不能做耗时操作,这是铁律。
正确的做法是:
- 中断里只设置标志位
- 在主循环或任务中处理通知
- 或者用「延迟通知」机制,把事件排队
四、三种模式对比
| 模式 | 核心思想 | 典型应用场景 | C语言实现难点 |
|---|---|---|---|
| 单例模式 | 全局唯一实例 | 硬件外设管理、系统配置 | 多线程安全、内存管理 |
| 工厂模式 | 对象创建与使用分离 | 多类型设备驱动、协议解析 | 函数指针表维护、动态注册 |
| 观察者模式 | 一对多依赖通知 | 事件驱动、按键处理、数据流 | 中断安全、生命周期管理 |
五、知识体系图
下面这张图展示了三种设计模式在嵌入式C语言中的定位和关系:
写在最后
设计模式不是银弹,但它能帮你写出更清晰、更易维护的代码。我个人觉得,嵌入式工程师学设计模式,重点不是背模式,而是理解它解决的是什么问题。
单例解决「唯一性」,工厂解决「创建灵活性」,观察者解决「通知解耦」。你想想看,这三个问题是不是每天都在遇到?
下一节我们会继续聊剩下的几种模式,包括策略模式、状态模式等。但今天这三个,已经足够你在实际项目中用起来了。
公众号:蓝海资料掘金营,微信deep3321