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;
}
我的经验: 在裸机环境下,我习惯用静态变量代替malloc。这样既避免了堆碎片,又不需要考虑释放问题。比如: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——嗯,死机了。中断里不能做耗时操作,这是铁律。

正确的做法是:

  • 中断里只设置标志位
  • 在主循环或任务中处理通知
  • 或者用「延迟通知」机制,把事件排队
重要提醒: 观察者模式虽然灵活,但不要滥用。如果观察者太多,通知的开销会很大。我一般控制在10个以内。另外,注意观察者的生命周期——被销毁的观察者要及时移除,否则会变成野指针。

四、三种模式对比

模式 核心思想 典型应用场景 C语言实现难点
单例模式 全局唯一实例 硬件外设管理、系统配置 多线程安全、内存管理
工厂模式 对象创建与使用分离 多类型设备驱动、协议解析 函数指针表维护、动态注册
观察者模式 一对多依赖通知 事件驱动、按键处理、数据流 中断安全、生命周期管理

五、知识体系图

下面这张图展示了三种设计模式在嵌入式C语言中的定位和关系:

C语言设计模式(上)知识体系 单例模式 工厂模式 观察者模式 硬件外设 系统配置 设备驱动 协议解析 事件驱动 数据流 核心目标:解耦、复用、可维护 实现要点 函数指针表 静态变量 回调注册 生命周期管理

写在最后

设计模式不是银弹,但它能帮你写出更清晰、更易维护的代码。我个人觉得,嵌入式工程师学设计模式,重点不是背模式,而是理解它解决的是什么问题。

单例解决「唯一性」,工厂解决「创建灵活性」,观察者解决「通知解耦」。你想想看,这三个问题是不是每天都在遇到?

下一节我们会继续聊剩下的几种模式,包括策略模式、状态模式等。但今天这三个,已经足够你在实际项目中用起来了。

一句话总结: 设计模式是经验的结晶,不是理论的教条。用C语言实现设计模式,考验的是你对指针、内存和架构的理解深度。

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