模块化编程中的设计模式:单例、工厂、观察者与状态机

说实话,很多嵌入式工程师一听到「设计模式」四个字,第一反应是——那是 Java 那边的事吧?我当年也这么想。直到有一次我接手一个大型的物联网网关项目,代码量从几千行膨胀到十几万行,我才发现,没有模式约束的 C 代码,维护起来简直是一场噩梦。

今天我们就来聊聊 C 语言里最实用的四种设计模式。它们不是理论玩具,而是我实战中反复用到的利器。

一、单例模式:全局唯一,但别滥用

单例模式,说白了就是保证某个模块在整个系统中只有一个实例。在 C 里,我们通常用静态全局变量来实现。

我个人的习惯是:硬件外设驱动、系统时钟管理、日志模块,这些天然就应该只有一个实例的东西,用单例模式非常合适。

核心要点:单例模式的关键不是「只有一个」,而是「控制访问」。

经典实现方式

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

typedef struct {
    void (*log)(const char *msg);
    void (*set_level)(int level);
} Logger;

Logger* logger_get_instance(void);

#endif
// logger.c
#include "logger.h"

static Logger instance = {0};
static int initialized = 0;

static void default_log(const char *msg) {
    // 默认输出到串口
    printf("[LOG] %s\n", msg);
}

Logger* logger_get_instance(void) {
    if (!initialized) {
        instance.log = default_log;
        instance.set_level = NULL;
        initialized = 1;
    }
    return &instance;
}
我的经验:我曾经在一个多任务系统里直接用裸指针返回单例,结果被两个任务同时调用初始化函数,导致竞态条件。后来我加了一个互斥锁保护初始化过程,问题才解决。如果你在 RTOS 环境下使用单例,记得考虑线程安全。

什么时候不该用单例?

  • 当模块可能有多个实例时(比如多个传感器同型号)
  • 当单例导致测试困难时(单元测试里 mock 不掉)
  • 当单例隐藏了依赖关系时(代码里到处都能调用,耦合度飙升)

二、工厂模式:把「创建」和「使用」拆开

工厂模式解决的是一个问题:调用者不需要知道具体创建的是哪个对象。你想想看,如果你的代码里到处都是 if (type == A) createA() else if (type == B) createB(),那改一个类型就要改所有地方,多痛苦。

我在项目中遇到过这样一个场景:一个数据采集系统需要支持多种传感器,每种传感器的初始化流程完全不同。用工厂模式之后,新增一种传感器只需要加一个工厂分支,主逻辑完全不用动。

简单工厂实现

// sensor_factory.h
typedef struct Sensor {
    void (*init)(void);
    int (*read)(void);
} Sensor;

Sensor* sensor_create(int type);
// type: 0=温度, 1=湿度, 2=压力
// sensor_factory.c
#include "sensor_factory.h"

static Sensor temp_sensor = { temp_init, temp_read };
static Sensor humi_sensor = { humi_init, humi_read };
static Sensor press_sensor = { press_init, press_read };

Sensor* sensor_create(int type) {
    switch (type) {
        case 0: return &temp_sensor;
        case 1: return &humi_sensor;
        case 2: return &press_sensor;
        default: return NULL;
    }
}
注意:工厂模式在 C 里通常返回指针,但要注意生命周期管理。我见过有人返回栈上变量的地址,结果一调用就崩。建议返回静态变量或者堆上分配的内存,并明确谁负责释放。

三、观察者模式:事件驱动的核心

观察者模式,说白了就是「发布-订阅」。一个事件发生,多个订阅者收到通知。这在嵌入式里太常见了——按键按下、数据到达、状态变化,都需要通知多个模块。

我记得有一次做智能家居项目,一个温度传感器数据变化,需要同时更新显示、触发报警、记录日志。如果每个模块都去轮询传感器,那 CPU 就废了。观察者模式完美解决这个问题。

简易实现

// observer.h
#define MAX_OBSERVERS 10

typedef void (*event_handler)(int event_id, void *data);

typedef struct {
    event_handler handlers[MAX_OBSERVERS];
    int count;
} EventBus;

void eventbus_init(EventBus *bus);
int  eventbus_register(EventBus *bus, event_handler handler);
void eventbus_fire(EventBus *bus, int event_id, void *data);
// observer.c
#include "observer.h"

void eventbus_init(EventBus *bus) {
    bus->count = 0;
}

int eventbus_register(EventBus *bus, event_handler handler) {
    if (bus->count >= MAX_OBSERVERS) return -1;
    bus->handlers[bus->count++] = handler;
    return 0;
}

void eventbus_fire(EventBus *bus, int event_id, void *data) {
    for (int i = 0; i < bus->count; i++) {
        if (bus->handlers[i]) {
            bus->handlers[i](event_id, data);
        }
    }
}
避坑指南:我曾经在中断服务函数里直接调用 eventbus_fire,结果订阅者的回调函数里做了耗时操作,导致系统响应变慢。后来我改成在中断里只设置标志位,在主循环里统一派发事件。记住:不要在中断里做复杂回调

四、状态机模式:复杂逻辑的救星

状态机模式,我觉得是嵌入式开发里最实用的模式,没有之一。任何有「状态」的系统——通信协议、按键处理、设备控制——都离不开它。

我刚开始写代码时,喜欢用一堆 if-else 处理状态,结果代码又臭又长,改一个状态要翻半天。后来我学会了用「状态表」来实现状态机,清爽多了。

状态表实现

// state_machine.h
typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_ERROR,
    STATE_COUNT
} State;

typedef enum {
    EVENT_START,
    EVENT_STOP,
    EVENT_ERROR,
    EVENT_COUNT
} Event;

typedef State (*action_fn)(void);

typedef struct {
    State  current_state;
    action_fn actions[STATE_COUNT][EVENT_COUNT];
} StateMachine;
// state_machine.c
#include "state_machine.h"

static State on_start_idle(void) {
    printf("Idle -> Running\n");
    return STATE_RUNNING;
}

static State on_stop_running(void) {
    printf("Running -> Idle\n");
    return STATE_IDLE;
}

void sm_init(StateMachine *sm) {
    sm->current_state = STATE_IDLE;
    // 初始化状态表,未定义的事件返回当前状态
    for (int s = 0; s < STATE_COUNT; s++)
        for (int e = 0; e < EVENT_COUNT; e++)
            sm->actions[s][e] = NULL;

    sm->actions[STATE_IDLE][EVENT_START] = on_start_idle;
    sm->actions[STATE_RUNNING][EVENT_STOP] = on_stop_running;
}

void sm_handle_event(StateMachine *sm, Event event) {
    action_fn fn = sm->actions[sm->current_state][event];
    if (fn) {
        sm->current_state = fn();
    }
}
为什么状态表好?因为状态和事件是二维的,新增一个状态或事件,只需要扩展表格,不需要改逻辑代码。可维护性直接拉满。

知识体系总览

下面这张图是我自己总结的四种模式的关系。你看,它们解决的是不同层面的问题:

C语言模块化编程中的四种设计模式 单例模式 保证全局唯一实例 适用:日志、时钟、硬件驱动 注意:线程安全、生命周期 工厂模式 解耦创建与使用 适用:多类型设备、插件系统 注意:内存管理、扩展性 观察者模式 事件驱动,一对多通知 适用:按键、数据更新、报警 注意:不要在中断中回调 状态机模式 状态驱动,逻辑清晰 适用:协议解析、设备控制 推荐:状态表实现 共同目标:高内聚、低耦合、可维护 没有银弹,根据场景选择合适的模式

总结一下

这四种模式,我建议你从状态机开始练手,因为它最贴近嵌入式开发的实际需求。然后试试观察者模式,你会发现事件驱动的代码比轮询优雅得多。单例模式工厂模式,等你项目规模大了自然就会用到。

记住一点:设计模式是工具,不是教条。不要为了用模式而用模式。我见过有人在一个只有三个函数的模块里硬套工厂模式,结果代码量翻了三倍,维护起来更痛苦。合适的才是最好的。

最后分享一个习惯:我每次写新模块前,会先画一张简单的框图,标出哪些地方需要单例、哪些需要工厂、哪些需要观察者。画完之后再动手写代码,思路会清晰很多。你也试试?

公众号:蓝海资料掘金营,微信deep3321