3. 状态机设计模式:单例、工厂与策略的实战组合
各位同学,今天我们来聊聊状态机里的设计模式。说实话,我刚入行那会儿,写状态机就是一把梭——一个巨大的 switch-case,所有逻辑堆在一起。后来项目越做越大,代码改一次崩三次,我才意识到:好的架构不是写出来的,是设计出来的。
这一章,我重点讲三个模式:单例模式、工厂模式和策略模式。它们不是花架子,而是我在多个嵌入式项目中反复验证过的实战组合。
核心思想:状态机本身是单例,状态对象由工厂创建,状态内的动作由策略封装。三者各司其职,互不干扰。
3.1 单例模式:状态机就该只有一个
为什么状态机要用单例?你想想看,一个系统里跑两个相同状态机实例,那不乱套了?我曾在某个物联网网关项目里见过这种问题——两个实例同时处理同一个事件,结果状态互相覆盖,设备直接死机。
单例模式保证全局只有一个状态机实例。实现方式很简单:
// 状态机单例 - C语言实现
typedef struct {
state_t current_state;
event_queue_t queue;
// ... 其他成员
} state_machine_t;
static state_machine_t *instance = NULL;
state_machine_t* state_machine_get_instance(void) {
if (instance == NULL) {
instance = (state_machine_t*)malloc(sizeof(state_machine_t));
if (instance) {
memset(instance, 0, sizeof(state_machine_t));
instance->current_state = STATE_IDLE;
}
}
return instance;
}
个人经验:我习惯在单例里加一个初始化标志位。第一次调用时做完整初始化,后续只返回指针。这样能避免重复初始化带来的资源浪费。
嗯,这里要注意:单例不是万能药。如果你的系统里确实需要多个独立的状态机(比如每个外设一个),那就别硬套单例。我见过有人把整个系统塞进一个单例,结果耦合度爆炸——改一个模块,全盘重测。
3.2 工厂模式:让状态对象自己长出来
传统写法里,状态切换时我们手动 new 一个状态对象。但问题来了:如果新增一个状态,你得改所有切换逻辑。这违反了开闭原则。
工厂模式解决的就是这个问题。你把状态的创建逻辑集中到工厂里,状态机只问工厂要状态,不管它怎么造出来的。
// 状态工厂 - 根据状态ID创建对应状态对象
state_t* state_factory_create(state_id_t id) {
switch (id) {
case STATE_IDLE:
return (state_t*)idle_state_new();
case STATE_RUNNING:
return (state_t*)running_state_new();
case STATE_ERROR:
return (state_t*)error_state_new();
// 新增状态只需在这里加一行
default:
return NULL;
}
}
我曾经在一个智能家居项目里,用工厂模式管理 12 个状态。后来需求变更,加了 3 个新状态。你猜怎么着?我只改了工厂函数和新增的状态文件,状态机核心逻辑一行没动。这就是工厂模式的价值。
避坑指南:我曾经犯过一个错——在工厂里做状态对象的缓存。结果状态对象是有内部状态的,复用会导致数据污染。后来我改成每次创建新实例,虽然多了一点内存开销,但安全多了。
3.3 策略模式:把动作从状态里拆出来
很多同学写状态机,喜欢把动作直接写在状态处理函数里。比如:
// 反面教材:动作和状态耦合
void idle_state_handler(event_t *evt) {
if (evt->type == EVT_START) {
// 打开电机
motor_start();
// 记录日志
log_write("motor started");
// 发送通知
notify_send(MSG_MOTOR_ON);
// 切换状态
transition_to(STATE_RUNNING);
}
}
这样写有什么问题?动作逻辑没法复用。如果另一个状态也要打开电机,你得复制粘贴。更糟的是,测试时你得构造完整的状态环境才能测动作。
策略模式的做法是:把每个动作封装成独立的策略对象。状态机只负责编排策略,不关心策略内部怎么实现。
// 策略接口
typedef struct {
void (*execute)(void *context);
} action_strategy_t;
// 具体策略:启动电机
static void motor_start_execute(void *ctx) {
// 电机启动逻辑
gpio_set_level(MOTOR_PIN, 1);
delay_ms(10);
// 可以加更多细节...
}
action_strategy_t motor_start_strategy = {
.execute = motor_start_execute
};
// 状态处理时,直接调用策略
void idle_state_handler(event_t *evt) {
if (evt->type == EVT_START) {
motor_start_strategy.execute(NULL);
log_write_strategy.execute("motor started");
notify_send_strategy.execute(MSG_MOTOR_ON);
transition_to(STATE_RUNNING);
}
}
关键好处:策略可以单独测试、单独替换。我在一个工业控制项目里,把电机控制策略从「直接启动」换成「软启动」,只改了一个策略文件,状态机完全不受影响。
3.4 三者如何协同工作
好了,三个模式都讲完了。它们怎么配合?我画个流程:
- 状态机(单例)收到事件后,调用工厂创建目标状态对象
- 工厂根据状态ID,返回对应的状态实例
- 状态对象内部调用多个策略来执行具体动作
- 动作执行完毕,状态机切换当前状态指针
说白了,单例管「谁在用」,工厂管「怎么造」,策略管「怎么做」。三层分离,各管各的。
| 模式 | 职责 | 变化点 | 我常用的场景 |
|---|---|---|---|
| 单例 | 保证全局唯一 | 状态机实例数量 | 系统主控状态机 |
| 工厂 | 创建状态对象 | 状态种类增减 | 多状态切换系统 |
| 策略 | 封装具体动作 | 动作实现方式 | 硬件操作、通信协议 |
我的建议:刚开始别追求完美。先写出能跑的状态机,然后逐步把动作抽成策略,再把状态创建交给工厂。最后如果发现确实只需要一个实例,再套上单例。迭代优化,比一步到位靠谱得多。
这一章的内容就到这里。三个模式单独看都不难,但组合起来威力很大。你可以在自己的项目里试试,哪怕先从一个状态开始重构,也能感受到架构带来的好处。
可可精品资料库