3. 状态机设计模式:单例、工厂与策略的实战组合

各位同学,今天我们来聊聊状态机里的设计模式。说实话,我刚入行那会儿,写状态机就是一把梭——一个巨大的 switch-case,所有逻辑堆在一起。后来项目越做越大,代码改一次崩三次,我才意识到:好的架构不是写出来的,是设计出来的。

这一章,我重点讲三个模式:单例模式、工厂模式和策略模式。它们不是花架子,而是我在多个嵌入式项目中反复验证过的实战组合。

核心思想:状态机本身是单例,状态对象由工厂创建,状态内的动作由策略封装。三者各司其职,互不干扰。

状态机设计模式架构图 状态机(单例) 全局唯一实例 状态工厂(工厂模式) 按需创建状态对象 动作策略A 封装具体行为 动作策略B 封装具体行为 单例 工厂 策略

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 三者如何协同工作

好了,三个模式都讲完了。它们怎么配合?我画个流程:

  1. 状态机(单例)收到事件后,调用工厂创建目标状态对象
  2. 工厂根据状态ID,返回对应的状态实例
  3. 状态对象内部调用多个策略来执行具体动作
  4. 动作执行完毕,状态机切换当前状态指针

说白了,单例管「谁在用」,工厂管「怎么造」,策略管「怎么做」。三层分离,各管各的。

模式 职责 变化点 我常用的场景
单例 保证全局唯一 状态机实例数量 系统主控状态机
工厂 创建状态对象 状态种类增减 多状态切换系统
策略 封装具体动作 动作实现方式 硬件操作、通信协议

我的建议:刚开始别追求完美。先写出能跑的状态机,然后逐步把动作抽成策略,再把状态创建交给工厂。最后如果发现确实只需要一个实例,再套上单例。迭代优化,比一步到位靠谱得多。

这一章的内容就到这里。三个模式单独看都不难,但组合起来威力很大。你可以在自己的项目里试试,哪怕先从一个状态开始重构,也能感受到架构带来的好处。

可可精品资料库