12、状态机设计模式:单例模式、观察者模式、状态模式在C语言中的实现
说实话,很多嵌入式工程师写状态机,写着写着就成了一锅粥。全局变量满天飞,状态切换靠if-else堆砌,代码改一次就崩一次。我早年也踩过这个坑,后来才意识到——状态机本身也需要好的设计模式来约束。
这一章,咱们就聊聊三种最实用的设计模式:单例模式、观察者模式、状态模式。它们不是C++的专利,C语言一样能玩得转。而且,在状态机场景下,这些模式能帮你写出更干净、更可维护的代码。
12.1 单例模式:确保状态机实例唯一
先问个问题:你的系统里,同一个状态机需要存在多个实例吗?
大多数情况下,不需要。比如一个LED控制状态机、一个通信协议状态机,全局只有一个就够了。如果谁都能new一个出来,那状态就乱了。
单例模式,说白了就是保证一个类只有一个实例,并提供一个全局访问点。在C语言里,我们通常用静态变量来实现。
12.1.1 经典实现:懒汉式
我个人习惯用懒汉式——第一次使用时才创建实例。这样能避免启动时不必要的初始化开销。
/* singleton_state_machine.h */
typedef struct {
uint8_t current_state;
uint8_t event_queue[16];
uint8_t queue_head, queue_tail;
} StateMachine;
StateMachine* StateMachine_GetInstance(void);
/* singleton_state_machine.c */
static StateMachine instance = {0};
static uint8_t initialized = 0;
StateMachine* StateMachine_GetInstance(void) {
if (!initialized) {
instance.current_state = STATE_IDLE;
instance.queue_head = 0;
instance.queue_tail = 0;
initialized = 1;
}
return &instance;
}
你看,代码很简单。但有个坑——多线程环境下,两个任务同时调用GetInstance,可能都发现initialized == 0,然后各自初始化一遍。我曾经在一个RTOS项目里就遇到过这个问题,调试了两天才找到原因。
12.1.2 饿汉式实现
/* 饿汉式:编译期就初始化 */
static StateMachine instance = {
.current_state = STATE_IDLE,
.queue_head = 0,
.queue_tail = 0
};
StateMachine* StateMachine_GetInstance(void) {
return &instance;
}
嗯,这样就没有竞态条件了。代价是——不管用不用,实例都占着内存。对于资源紧张的MCU,你得权衡一下。
12.2 观察者模式:状态变化通知机制
状态机跑着跑着,状态变了。谁需要知道这个变化?
可能是LED驱动要更新显示,可能是日志模块要记录,也可能是另一个状态机要联动。如果让状态机直接调用这些模块,耦合度就太高了。
观察者模式就是来解决这个问题的。状态机作为「被观察者」,只负责发布事件。其他模块作为「观察者」,订阅自己感兴趣的事件。双方互不依赖。
12.2.1 用函数指针实现观察者
C语言没有类,但函数指针就是我们的利器。
/* observer.h */
#define MAX_OBSERVERS 8
typedef void (*StateChangeHandler)(uint8_t old_state, uint8_t new_state, void* context);
typedef struct {
StateChangeHandler handler;
void* context;
} Observer;
typedef struct {
Observer observers[MAX_OBSERVERS];
uint8_t count;
} Observable;
/* observer.c */
void Observable_Init(Observable* obs) {
obs->count = 0;
}
int Observable_Subscribe(Observable* obs, StateChangeHandler handler, void* context) {
if (obs->count >= MAX_OBSERVERS) return -1;
obs->observers[obs->count].handler = handler;
obs->observers[obs->count].context = context;
obs->count++;
return 0;
}
void Observable_Notify(Observable* obs, uint8_t old_state, uint8_t new_state) {
for (uint8_t i = 0; i < obs->count; i++) {
obs->observers[i].handler(old_state, new_state, obs->observers[i].context);
}
}
我在一个智能家居项目里用过这个模式。主状态机管理着「安防模式」「离家模式」「睡眠模式」,每次切换,窗帘、灯光、空调各自响应。如果不用观察者模式,状态机里得写一堆if-else,想想就头疼。
12.3 状态模式:把状态行为封装成对象
传统的switch-case状态机,状态越多,case分支越臃肿。而且,每个case里可能还嵌套着子switch,代码可读性直线下降。
状态模式的思路是:把每个状态的行为封装成一个独立的结构体,里面放上该状态下的处理函数指针。状态切换时,只需要换掉这个结构体指针就行。
12.3.1 状态接口定义
/* state.h */
typedef struct State State;
typedef struct {
void (*on_entry)(State* self, void* context);
void (*on_exit)(State* self, void* context);
uint8_t (*handle_event)(State* self, uint8_t event, void* context);
} StateVtable;
struct State {
const StateVtable* vtable;
uint8_t state_id;
};
12.3.2 具体状态实现
/* idle_state.c */
static void Idle_OnEntry(State* self, void* context) {
/* 进入空闲状态,关闭所有输出 */
printf("Enter IDLE state\n");
}
static void Idle_OnExit(State* self, void* context) {
printf("Exit IDLE state\n");
}
static uint8_t Idle_HandleEvent(State* self, uint8_t event, void* context) {
switch (event) {
case EVT_START:
/* 切换到运行状态 */
return STATE_RUNNING;
case EVT_ERROR:
return STATE_ERROR;
default:
return STATE_IDLE; /* 不处理 */
}
}
const StateVtable idle_vtable = {
.on_entry = Idle_OnEntry,
.on_exit = Idle_OnExit,
.handle_event = Idle_HandleEvent
};
State idle_state = {
.vtable = &idle_vtable,
.state_id = STATE_IDLE
};
12.3.3 状态机上下文
/* state_machine.c */
typedef struct {
State* current_state;
void* context_data;
} StateMachineContext;
void StateMachine_Transition(StateMachineContext* ctx, State* new_state) {
if (ctx->current_state && ctx->current_state->vtable->on_exit) {
ctx->current_state->vtable->on_exit(ctx->current_state, ctx->context_data);
}
ctx->current_state = new_state;
if (new_state && new_state->vtable->on_entry) {
new_state->vtable->on_entry(new_state, ctx->context_data);
}
}
uint8_t StateMachine_HandleEvent(StateMachineContext* ctx, uint8_t event) {
if (!ctx->current_state) return 0;
uint8_t next_id = ctx->current_state->vtable->handle_event(
ctx->current_state, event, ctx->context_data
);
/* 根据next_id查找对应的State对象,然后切换 */
/* ... */
}
你想想看,这样写的好处是什么?每个状态都是独立的模块,新增一个状态,只需要写一个新的xxx_state.c,不用动其他代码。我在一个通信协议栈里用了这个模式,状态从最初的5个扩展到15个,代码依然清晰。
12.4 三种模式的对比与选择
| 模式 | 核心思想 | 适用场景 | 典型问题 |
|---|---|---|---|
| 单例模式 | 确保唯一实例 | 全局唯一的状态机,如系统主控状态机 | 多线程竞态、全局变量污染 |
| 观察者模式 | 一对多通知 | 状态变化需要通知多个模块 | 回调执行时间过长、内存泄漏 |
| 状态模式 | 状态行为封装 | 状态多、行为复杂、频繁增删状态 | 代码量增加、函数指针开销 |
说实话,这三个模式不是互斥的。我经常把它们组合使用——单例模式保证状态机唯一,状态模式组织内部逻辑,观察者模式对外通知变化。三者配合,代码质量能上一个台阶。
12.5 知识体系总览
下面这张图,帮你理清这三种模式在状态机中的位置和关系。
嗯,这张图把三个模式的关系说清楚了。单例模式管「唯一性」,状态模式管「内部行为」,观察者模式管「外部通知」。各司其职,互不干扰。
好了,这一章的内容就到这里。三种模式,每个都有它的用武之地。你在实际项目中,可以根据状态机的复杂度和需求灵活选用。记住,模式是工具,不是目的。写出好维护的代码,才是我们的目标。