标签 嵌入式 下的文章

用嵌入式 C 重新理解工厂模式:它解决的从来不只是“创建对象”

很多人第一次接触工厂模式,是从 Java 里的 new、接口、继承开始的。
但到了嵌入式 C,既没有 new,也没有类,很多人就会产生疑问:

工厂模式在嵌入式里到底有什么用?

其实只要把“对象”换成“模块”,这个模式一下就变得很接地气了。


一、先说结论:工厂模式到底在解决什么?

工厂模式表面上是在“创建对象”,但它真正想解决的问题是:

把“一个模块是怎么被组装出来的”这件事,从“这个模块怎么被使用”里面拆出去。

在嵌入式项目里,一个“对象”通常不是 Java 里的某个 class,而是下面这些东西的组合:

对象 / 模块
=
状态数据 struct
+ 操作接口 function pointer
+ 硬件资源
+ 依赖模块
+ 初始化过程
+ 生命周期

比如一个温度传感器模块,看起来只是“读温度”,但真正把它跑起来,背后可能要处理:

SHT30
├── 使用 I2C1
├── 地址 0x44
├── 上电后需要等待
├── 需要软复位
├── 需要 CRC 校验
└── 还要处理通信错误

业务代码真正关心的,其实只是:

float temperature = 0.0f;
sensor_read_temperature(sensor, &temperature);

它不应该天天操心:

  • 到底是哪颗传感器;
  • 挂在哪条 I2C 总线上;
  • 地址是多少;
  • 初始化要发什么命令;
  • 复位顺序是什么;
  • 换板子后底层怎么改。

这就是工厂模式最有价值的地方。


二、先别急着上模式,最简单的代码往往最好

假设一个产品支持两种温度传感器:

typedef enum
{
    SENSOR_TYPE_SHT30,
    SENSOR_TYPE_TMP117
} SensorType;

项目刚开始时,最直接的写法可能是这样:

int app_init(SensorType type)
{
    if (type == SENSOR_TYPE_SHT30)
    {
        i2c_init(I2C1);
        sht30_init(I2C1, 0x44);
    }
    else if (type == SENSOR_TYPE_TMP117)
    {
        i2c_init(I2C2);
        tmp117_init(I2C2, 0x48);
    }

    return 0;
}

这段代码有没有问题?

没有。

甚至在项目规模还小的时候,我反而建议就这么写。

设计模式不是为了把简单代码写复杂。

如果创建逻辑就三五行,而且以后大概率也不会变,硬套一层工厂,只会让代码更绕。

真正应该考虑工厂模式,是当下面这些情况开始出现时:

  • 支持的硬件型号越来越多;
  • if-else / switch 越来越长;
  • 每种模块的初始化过程越来越复杂;
  • 业务代码里开始到处出现硬件细节;
  • 同一段创建逻辑被多处复制;
  • 做单元测试时,很难把真实硬件替换掉。

这时候,问题就不再是“代码不好看”,而是:

变化开始污染稳定代码。


三、嵌入式 C 里的“对象”到底是什么?

Java/C++ 里讲设计模式,经常会出现:

  • 接口;
  • 抽象类;
  • 对象;
  • 多态。

C 语言里没有这些语法糖,但同样可以表达。

例如,我们定义一个传感器接口:

typedef struct Sensor Sensor;

typedef struct
{
    int (*init)(Sensor *self);
    int (*read_temperature)(Sensor *self, float *value);
    int (*deinit)(Sensor *self);
} SensorOps;

struct Sensor
{
    const SensorOps *ops;
    void *driver_data;
};

业务代码只通过接口使用它:

int sensor_read_temperature(Sensor *sensor, float *value)
{
    if (sensor == NULL ||
        sensor->ops == NULL ||
        sensor->ops->read_temperature == NULL)
    {
        return -1;
    }

    return sensor->ops->read_temperature(sensor, value);
}

这套写法里,Sensor 可以理解为“接口对象”,而 SensorOps 就是一张操作函数表。

所以以后看到面向对象里的概念,可以先这样翻译:

面向对象概念嵌入式 C 中常见写法
对象struct
接口一组函数指针
多态不同 ops
构造函数xxx_init() / xxx_create()
依赖struct *
工厂xxx_factory_create()

只要这层映射能建立起来,很多设计模式放到 C 里就不神秘了。


四、简单工厂:先把“选哪个实现”集中起来

假设现在支持:

SHT30
TMP117

我们可以先把创建逻辑从业务层抽出来。

1. 定义统一入口

typedef enum
{
    SENSOR_TYPE_SHT30,
    SENSOR_TYPE_TMP117
} SensorType;

Sensor *sensor_factory_create(SensorType type);

2. 工厂里负责选择具体实现

Sensor *sensor_factory_create(SensorType type)
{
    switch (type)
    {
        case SENSOR_TYPE_SHT30:
            return sht30_create();

        case SENSOR_TYPE_TMP117:
            return tmp117_create();

        default:
            return NULL;
    }
}

3. 业务代码只拿接口

static Sensor *g_sensor;

void app_init(void)
{
    g_sensor = sensor_factory_create(g_config.sensor_type);

    if (g_sensor == NULL)
    {
        system_fault();
        return;
    }

    g_sensor->ops->init(g_sensor);
}

此时,业务层已经不再关心:

SHT30 怎么初始化
TMP117 怎么初始化
以后新增 BME280 怎么办

它只关心:

我需要一个 Sensor

这就是简单工厂最直接的价值。


五、工厂模式并没有消灭复杂度,只是把复杂度搬到了正确的位置

很多人学完工厂模式后,会得到一个错误印象:

工厂模式就是为了消灭 if-else。

其实不是。

你看刚才的代码:

switch (type)
{
    case SENSOR_TYPE_SHT30:
        ...
    case SENSOR_TYPE_TMP117:
        ...
}

判断还在。

工厂模式真正做的是把原来混在业务层里的这些东西:

业务逻辑
传感器类型判断
硬件资源选择
初始化流程
错误处理

拆开成:

app.c
└── 业务逻辑

sensor_factory.c
└── 选择哪个传感器

sht30.c
└── SHT30 自己的初始化

tmp117.c
└── TMP117 自己的初始化

复杂度没有消失。

但它被放到了更合适的位置。

这在实际项目里非常重要,因为软件架构很多时候并不是“消灭复杂度”,而是:

把不同类型的复杂度隔离开。


六、嵌入式里简单工厂往往比“标准 OO 写法”更实用

很多 MCU 项目里,对象生命周期非常固定:

上电
→ 初始化
→ 一直运行
→ 断电

这和服务器程序不一样。

我们通常不需要频繁地动态创建、销毁对象,也不一定需要 malloc()。

因此完全可以这样写:

static Sht30 g_sht30;
static Tmp117 g_tmp117;

然后工厂只是返回静态实例:

Sensor *sensor_factory_get(SensorType type)
{
    switch (type)
    {
        case SENSOR_TYPE_SHT30:
            return sht30_get_instance();

        case SENSOR_TYPE_TMP117:
            return tmp117_get_instance();

        default:
            return NULL;
    }
}

这种方式对嵌入式很友好:

  • 内存占用确定;
  • 不会产生堆碎片;
  • 生命周期清楚;
  • 调试简单;
  • 实时性更容易控制。

所以不要照搬 Java 的“每次 new 一个对象”。

在嵌入式里,模式要服务于资源约束,而不是反过来。


七、什么时候简单工厂开始变得难维护?

当每种设备的“创建过程”越来越复杂时,简单工厂很快会变成这样:

Sensor *sensor_factory_create(SensorType type)
{
    switch (type)
    {
        case SENSOR_TYPE_SHT30:
        {
            i2c_init(...);
            gpio_init(...);
            delay_ms(...);

            sht30_reset(...);
            sht30_wait_ready(...);
            sht30_read_serial(...);
            sht30_config_measurement(...);

            return ...;
        }

        case SENSOR_TYPE_BME280:
        {
            spi_init(...);
            gpio_init(...);

            bme280_reset(...);
            bme280_read_calibration(...);
            bme280_config(...);

            return ...;
        }

        case SENSOR_TYPE_DS18B20:
        {
            ...
        }
    }
}

这时候,问题已经不是 switch 长不长了。

真正的问题是:

每一种对象的创建过程本身就已经很复杂。

这时可以继续拆。


八、工厂方法:把每一种实现的创建过程单独管理

在 C 里,不需要死磕“Factory 接口”这种面向对象形式。

更实用的理解是:

每种具体实现,都有自己的 creator。

比如:

Sensor *sht30_create(void);
Sensor *tmp117_create(void);
Sensor *bme280_create(void);

SHT30 自己负责自己的初始化:

Sensor *sht30_create(void)
{
    static Sht30 instance;

    memset(&instance, 0, sizeof(instance));

    instance.base.ops = &g_sht30_ops;
    instance.bus = i2c_factory_get(I2C_BUS_1);
    instance.address = 0x44;

    if (sht30_hw_reset(&instance) != 0)
    {
        return NULL;
    }

    if (sht30_read_serial_number(&instance) != 0)
    {
        return NULL;
    }

    if (sht30_config_measurement(&instance) != 0)
    {
        return NULL;
    }

    return &instance.base;
}

TMP117 则完全独立:

Sensor *tmp117_create(void)
{
    static Tmp117 instance;

    memset(&instance, 0, sizeof(instance));

    instance.base.ops = &g_tmp117_ops;
    instance.bus = i2c_factory_get(I2C_BUS_2);
    instance.address = 0x48;

    if (tmp117_reset(&instance) != 0)
    {
        return NULL;
    }

    if (tmp117_set_conversion_mode(&instance) != 0)
    {
        return NULL;
    }

    return &instance.base;
}

总工厂只负责“找谁来创建”:

Sensor *sensor_factory_create(SensorType type)
{
    switch (type)
    {
        case SENSOR_TYPE_SHT30:
            return sht30_create();

        case SENSOR_TYPE_TMP117:
            return tmp117_create();

        default:
            return NULL;
    }
}

这时候职责就很清晰:

总工厂
└── 决定“选谁”

具体 creator
└── 决定“怎么创建”

九、再进一步:用函数指针表代替越来越长的 switch

当驱动数量继续增加,可以做一张注册表。

typedef Sensor *(*SensorCreateFunc)(void);

typedef struct
{
    SensorType type;
    SensorCreateFunc create;
} SensorFactoryEntry;

注册具体实现:

static const SensorFactoryEntry g_sensor_factory_table[] =
{
    { SENSOR_TYPE_SHT30,  sht30_create  },
    { SENSOR_TYPE_TMP117, tmp117_create },
    { SENSOR_TYPE_BME280, bme280_create }
};

统一查表:

Sensor *sensor_factory_create(SensorType type)
{
    size_t count =
        sizeof(g_sensor_factory_table) /
        sizeof(g_sensor_factory_table[0]);

    for (size_t i = 0; i < count; i++)
    {
        if (g_sensor_factory_table[i].type == type)
        {
            return g_sensor_factory_table[i].create();
        }
    }

    return NULL;
}

从结构上看,这其实就是:

type
↓
查表
↓
creator function
↓
具体实现

在嵌入式项目里,类似思路到处都能见到:

  • driver table;
  • device table;
  • protocol table;
  • command table;
  • init table。

很多时候,你已经在用“工厂思想”,只是没给它起这个名字。


十、工厂模式最适合的场景之一:驱动层

驱动层经常有一个特点:

上层接口稳定,底层实现变化频繁。

例如统一定义一个存储接口:

typedef struct Storage Storage;

typedef struct
{
    int (*read)(
        Storage *self,
        uint32_t addr,
        void *buf,
        uint32_t len);

    int (*write)(
        Storage *self,
        uint32_t addr,
        const void *buf,
        uint32_t len);

    int (*erase)(
        Storage *self,
        uint32_t addr,
        uint32_t len);
} StorageOps;

struct Storage
{
    const StorageOps *ops;
};

底层可能有:

SPI Flash
QSPI Flash
EEPROM
FRAM
文件模拟器

业务层只关心:

storage->ops->read(...);
storage->ops->write(...);

到底用哪一种,可以交给工厂。

这类场景通常比“为了创建一个普通 struct”更值得上工厂模式。


十一、抽象工厂:一次切换一整套平台实现

抽象工厂听起来很抽象,但放到嵌入式里其实很好理解。

假设同一套业务需要支持:

STM32F4
STM32H7
NXP RT1062
PC 仿真环境

每个平台都有自己的一整套底层实现:

UART
SPI
GPIO
Flash
Timer

这时我们真正想切换的,不是一个单独的 UART,而是一整套平台能力。

可以定义:

typedef struct
{
    Uart  *(*create_uart)(int id);
    Spi   *(*create_spi)(int id);
    Gpio  *(*create_gpio)(int id);
    Flash *(*create_flash)(int id);
} BoardFactory;

STM32 平台:

static const BoardFactory g_stm32_factory =
{
    .create_uart  = stm32_uart_create,
    .create_spi   = stm32_spi_create,
    .create_gpio  = stm32_gpio_create,
    .create_flash = stm32_flash_create
};

PC 测试平台:

static const BoardFactory g_host_factory =
{
    .create_uart  = mock_uart_create,
    .create_spi   = mock_spi_create,
    .create_gpio  = mock_gpio_create,
    .create_flash = file_flash_create
};

业务代码:

void system_init(const BoardFactory *factory)
{
    Uart *uart = factory->create_uart(0);
    Flash *flash = factory->create_flash(0);

    protocol_init(uart);
    config_manager_init(flash);
}

这样就能做到:

同一套业务代码
├── 跑在 STM32 上
└── 跑在 PC 单元测试里

这个场景可以把抽象工厂理解成:

“平台级资源供应商”。


十二、理解工厂模式后,再看 DI 就很顺了

依赖注入(Dependency Injection,DI)经常和 Spring、IOC、反射绑在一起讲,容易让嵌入式开发者觉得很远。

其实把外壳拿掉以后,DI 的核心很简单:

模块不要自己去找依赖,而是由外部把依赖交给它。

举个例子。

不使用 DI

void motor_controller_init(void)
{
    pwm_init(TIM1);
    encoder_init(TIM2);
    current_sensor_init(ADC1);
}

这段代码的问题是:

MotorController
已经知道
PWM = TIM1
Encoder = TIM2
CurrentSensor = ADC1

换板子麻烦,做 PC 单元测试更麻烦。


十三、用 C 语言做最朴素的 DI

先把依赖显式写出来:

typedef struct
{
    Pwm *pwm;
    Encoder *encoder;
    CurrentSensor *current_sensor;
} MotorControllerConfig;

MotorController:

typedef struct
{
    MotorControllerConfig config;
    float target_speed;
} MotorController;

初始化:

int motor_controller_init(
    MotorController *self,
    const MotorControllerConfig *config)
{
    if (self == NULL || config == NULL)
    {
        return -1;
    }

    self->config = *config;
    self->target_speed = 0.0f;

    return 0;
}

注意,此时 MotorController 已经不知道:

TIM1
TIM2
ADC1
STM32 HAL

它只知道:

我需要一个 PWM
我需要一个 Encoder
我需要一个 CurrentSensor

至于这些东西是谁创建的,由系统启动层负责。


十四、嵌入式里最实用的 DI:Composition Root

我们可以在 app_bootstrap.c 或 system_init.c 里集中组装整个系统。

static Pwm g_pwm;
static Encoder g_encoder;
static CurrentSensor g_current_sensor;
static MotorController g_motor;

void app_bootstrap(void)
{
    stm32_pwm_create(&g_pwm, TIM1, 1);

    stm32_encoder_create(&g_encoder, TIM2);

    stm32_current_sensor_create(
        &g_current_sensor,
        ADC1,
        3);

    MotorControllerConfig config =
    {
        .pwm = &g_pwm,
        .encoder = &g_encoder,
        .current_sensor = &g_current_sensor
    };

    motor_controller_init(
        &g_motor,
        &config);
}

有人可能会说:

这不就是手工初始化吗?

没错。

它就是手工 DI。

而且对于绝大多数 MCU 项目来说,这种方式通常已经足够好。

因为它把整个系统的依赖关系集中在一个地方:

app_bootstrap.c
├── 创建 PWM
├── 创建 Encoder
├── 创建 CurrentSensor
└── 注入 MotorController

这比在各个模块内部到处调用 xxx_get_instance() 更清楚。


十五、为什么依赖注入比 Service Locator 更容易维护?

很多嵌入式项目喜欢这样写:

void motor_controller_init(void)
{
    Pwm *pwm = pwm_get_instance();
    Encoder *encoder = encoder_get_instance();
}

这种方式表面上也挺方便。

但问题是依赖被藏起来了。

从函数签名:

motor_controller_init();

你根本不知道它依赖:

PWM
Encoder
ADC
Logger
Flash

而 DI 会把依赖显式暴露出来:

motor_controller_init(&motor, &config);

看 config 就能知道这个模块需要什么。

可以简单记成:

Service Locator
= 模块自己去找依赖

Dependency Injection
= 外部把依赖送进模块

大型项目里,显式依赖通常更容易维护和测试。


十六、Factory 和 DI 到底是什么关系?

可以这样理解:

Factory
解决的是:
“一个模块怎么创建”

而 DI / Composition Root 解决的是:

“整个系统怎么组装”

例如:

              app_bootstrap
                    |
        +-----------+-----------+
        |           |           |
     PWM Factory  Encoder    ADC Factory
        |           |           |
        +-----------+-----------+
                    |
                    v
            MotorController
                    |
                    v
                   App

工厂负责产生模块。

DI 负责把模块连起来。

这两个思想是可以一起使用的。


十七、C 没有反射,照样能做“配置驱动”

Java 里的 DI 容器经常依赖反射,根据配置动态创建类。

C 语言没有这种机制,但并不代表做不了类似的事情。

可以用:

ID
+
注册表
+
函数指针

代替一部分反射能力。

例如:

typedef enum
{
    DRIVER_SHT30,
    DRIVER_BME280,
    DRIVER_MPU6050
} DriverId;

typedef void *(*DriverCreateFunc)(void);

typedef struct
{
    DriverId id;
    DriverCreateFunc create;
} DriverFactoryEntry;

注册:

static const DriverFactoryEntry g_driver_table[] =
{
    { DRIVER_SHT30,   sht30_create   },
    { DRIVER_BME280,  bme280_create  },
    { DRIVER_MPU6050, mpu6050_create }
};

于是就有:

配置里的 driver id
↓
查注册表
↓
找到 create 函数
↓
创建对应驱动

它不是语言级反射,但从架构目的上看,已经能实现类似的“配置驱动创建”。


十八、一个完整的小例子:环境监测设备

假设我们做一个环境监测设备。

结构如下:

Application
    |
    v
EnvironmentService
    |
    v
Sensor
    |
    +------ SHT30
    |
    +------ BME280
    |
    v
I2C
    |
    v
STM32 HAL

1. I2C 接口

typedef struct I2c I2c;

typedef struct
{
    int (*write)(
        I2c *self,
        uint8_t addr,
        const uint8_t *data,
        size_t len);

    int (*read)(
        I2c *self,
        uint8_t addr,
        uint8_t *data,
        size_t len);
} I2cOps;

struct I2c
{
    const I2cOps *ops;
    void *context;
};

2. Sensor 接口

typedef struct Sensor Sensor;

typedef struct
{
    int (*read_temperature)(
        Sensor *self,
        float *value);

    int (*read_humidity)(
        Sensor *self,
        float *value);
} SensorOps;

struct Sensor
{
    const SensorOps *ops;
};

3. SHT30 实现

typedef struct
{
    Sensor base;
    I2c *i2c;
    uint8_t address;
} Sht30;

创建函数:

Sensor *sht30_create(
    I2c *i2c,
    uint8_t address)
{
    static Sht30 sensor;

    memset(&sensor, 0, sizeof(sensor));

    sensor.base.ops = &g_sht30_ops;
    sensor.i2c = i2c;
    sensor.address = address;

    if (sht30_reset(&sensor) != 0)
    {
        return NULL;
    }

    return &sensor.base;
}

注意这里:

I2c *i2c

本身就是依赖注入。

SHT30 只知道“我需要一个 I2C”,并不知道底层到底是:

STM32 I2C1
STM32 I2C2
Mock I2C

4. Sensor Factory

typedef enum
{
    SENSOR_SHT30,
    SENSOR_BME280
} SensorType;
Sensor *sensor_factory_create(
    SensorType type,
    I2c *i2c)
{
    switch (type)
    {
        case SENSOR_SHT30:
            return sht30_create(i2c, 0x44);

        case SENSOR_BME280:
            return bme280_create(i2c, 0x76);

        default:
            return NULL;
    }
}

5. 业务层

typedef struct
{
    Sensor *sensor;
} EnvironmentService;
void environment_service_init(
    EnvironmentService *self,
    Sensor *sensor)
{
    self->sensor = sensor;
}

业务代码只操作统一接口:

int environment_service_run(
    EnvironmentService *self)
{
    float temperature = 0.0f;

    return self->sensor->ops->read_temperature(
        self->sensor,
        &temperature);
}

6. 系统组装

static I2c g_i2c1;
static EnvironmentService g_env_service;

void app_bootstrap(void)
{
    I2c *i2c =
        stm32_i2c_create(
            &g_i2c1,
            I2C1);

    Sensor *sensor =
        sensor_factory_create(
            g_config.sensor_type,
            i2c);

    environment_service_init(
        &g_env_service,
        sensor);
}

现在整个系统的边界非常清楚:

app_bootstrap
├── 创建 I2C
├── 创建 Sensor
└── 注入 EnvironmentService

而业务层已经不再关心:

STM32 HAL
SHT30
0x44
I2C1

十九、为什么这种写法特别适合单元测试?

生产环境:

Sensor *sensor =
    sensor_factory_create(
        SENSOR_SHT30,
        stm32_i2c);

单元测试:

Sensor *sensor =
    fake_sensor_create();

假传感器:

static int fake_read_temperature(
    Sensor *self,
    float *value)
{
    *value = 25.0f;
    return 0;
}

测试业务层:

void test_environment_service(void)
{
    Sensor *sensor = fake_sensor_create();

    EnvironmentService service;

    environment_service_init(
        &service,
        sensor);

    /* 后续断言 */
}

真实硬件和测试替身都实现同一个接口。

业务代码不用改。

这就是“依赖抽象,而不是依赖具体实现”带来的直接收益。


二十、到底什么时候该用哪一种?

可以用下面这个判断思路:

创建过程很简单吗?
|
+-- 是 --> 直接 xxx_init()
|
+-- 否
    |
    +-- 需要根据类型选择不同实现吗?
        |
        +-- 是 --> 简单工厂
        |
        +-- 否 --> 继续看创建过程是否复杂

如果每种实现的创建过程都很复杂:

Simple Factory
        |
        v
拆成多个 creator
        |
        v
Factory Method 思路

如果要整体切换一组相关底层能力:

UART + SPI + GPIO + Flash
一起切换
        |
        v
Abstract Factory

如果整个系统模块很多,而且依赖关系复杂:

Composition Root / DI

二十一、把这几个模式连起来,其实是一条很自然的演进路线

可以把它们看成几个复杂度等级。

Level 0:直接初始化

sht30_init();

适合:

简单、固定、不会变

Level 1:简单工厂

sensor_factory_create(type);

解决:

“选哪个实现”

Level 2:Factory Method 思路

sht30_create();
bme280_create();
tmp117_create();

解决:

“每一种实现怎么复杂地创建”

Level 3:抽象工厂

board_factory->create_uart();
board_factory->create_spi();
board_factory->create_flash();

解决:

“整套平台实现一起切换”

Level 4:DI / Composition Root

App
├── Logger
│   └── UART
├── Storage
│   └── Flash
└── Motor
    ├── PWM
    ├── Encoder
    └── ADC

解决:

“整个系统怎么组装”

二十二、真正应该记住的不是模式名字,而是“变化边界”

做嵌入式项目时,可以经常问自己一个问题:

哪些东西是稳定的,哪些东西是经常变化的?

例如:

稳定:
温控算法

变化:
SHT30 -> TMP117
I2C1 -> I2C2
STM32 -> NXP
真实设备 -> Mock

那架构就应该让稳定部分尽量只依赖稳定接口。

          TemperatureControl
                 |
                 v
              Sensor
                 ^
                 |
       +---------+---------+
       |                   |
     SHT30               TMP117
       ^                   ^
       +------ Factory -----+

Factory 最重要的意义,其实不是“创建对象”。

而是:

在稳定代码和易变创建逻辑之间建立一道边界。


二十三、嵌入式项目尤其要防止“设计模式过度”

学完设计模式以后,很容易走向另一个极端:

GPIOFactory
ClockFactory
DelayFactory
RegisterFactory
PinFactory
...

结果原本很简单的代码:

Led led;

led_init(
    &led,
    GPIOA,
    GPIO_PIN_5);

被改成:

Led *led =
    led_factory_create(
        LED_FACTORY_INSTANCE,
        LED_TYPE_NORMAL,
        GPIO_PROVIDER_STM32,
        ...);

这通常不是更好的设计。

工厂模式不是为了让代码看起来“高级”。

它真正适合的场景是:

创建已经复杂到了值得单独管理。

如果一个模块初始化只有两三行,而且实现永远固定,就直接写。

嵌入式开发最怕的不是代码“不够抽象”,而是抽象层太多以后:

  • 调试路径变长;
  • 资源关系看不清;
  • 初始化顺序隐藏;
  • 新人不敢改;
  • 出问题时找不到实际执行路径。

设计模式应该降低复杂度,而不是制造复杂度。


二十四、最后只记 5 句话

如果整篇文章只留下几句话,可以记下面这些。

1. struct + 函数指针,就是 C 世界里最常见的“对象 + 接口”

很多面向对象设计模式,都可以用这套方式落地。

2. Factory 的核心不是 new

Factory 真正做的是:

把“怎么得到一个能工作的模块”从“怎么使用模块”中拆出去。

3. 简单工厂解决“选哪个”,Factory Method 解决“怎么复杂地创建”

这是嵌入式项目里最实用的理解方式。

4. 抽象工厂很适合 BSP / HAL / 多平台适配

因为它天然适合“一整套底层实现一起替换”。

5. DI 不是 Spring,也不是反射

DI 的本质是:

模块声明自己需要什么,而不是自己跑出去找什么。

系统组装层负责创建和连接它们。


结语

工厂模式真正值得学的,不是那几个类名,也不是 Factory 这个后缀。

真正有价值的是它背后的思路:

创建和使用分离
变化和稳定分离
组装和业务分离

在嵌入式 C 里,这种思想通常会落到下面这些具体形式里:

struct
函数指针
静态实例
creator 函数
注册表
app_bootstrap.c
system_init.c

最终理想的结构应该是:

                app_bootstrap.c
                       |
              创建 + 配置 + 连接
                       |
        +--------------+--------------+
        |              |              |
      Service        Driver         Storage
        |              |              |
        v              v              v
     Interface      Interface      Interface
        ^              ^              ^
        |              |              |
     可替换实现      可替换硬件      可替换后端

可以把 app_bootstrap.c 理解成一个特殊的地方:

它知道整个系统是怎么组装起来的。

而其他模块,只需要知道自己的接口、自己的职责,以及自己依赖什么。

当代码能做到这一点时,工厂模式就不再只是一个“设计模式题目”,而是真正开始参与嵌入式软件架构设计了。


嵌入式里的单例:别只盯着“对象只有一个”,先搞清楚“资源到底归谁管”

很多人第一次接触单例模式,记住的往往是这样一套写法:

class Singleton {
public:
    static Singleton& instance()
    {
        static Singleton obj;
        return obj;
    }

private:
    Singleton() = default;
};

然后脑子里很自然地形成一个等式:

单例模式 = 一个类只能创建一个对象

这个理解不能说错,但如果放到嵌入式开发里,就有点停留在表面了。

因为嵌入式系统面对的不是纯粹的软件对象,而是 UART、SPI、I2C、DMA、Flash、ADC、PWM 这些真实存在的硬件资源。

所以我更愿意这样理解单例:

嵌入式里的单例,不要从“对象只能有一个”出发,而要从“资源只能有一个 Owner”出发。

这句话听起来有点抽象,下面用一个 UART 的例子把它拆开。


先来看一个很常见的场景

假设 MCU 上只有一个 USART1。

现在系统里有两个模块需要使用它:

Logger ----\
            ---> USART1
Modem  ----/

Logger 用它打印日志,Modem 用它发送通信数据。

如果按照传统单例模式的思路,我们可能会把 UART 驱动写成这样:

class Uart1 {
public:
    static Uart1& instance()
    {
        static Uart1 uart;
        return uart;
    }

    void send(const uint8_t* data, size_t len);

private:
    Uart1() = default;
};

然后 Logger 这么用:

void Logger::write(const char* msg)
{
    Uart1::instance().send(
        reinterpret_cast<const uint8_t*>(msg),
        strlen(msg));
}

Modem 也这么用:

void Modem::sendPacket(const uint8_t* data, size_t len)
{
    Uart1::instance().send(data, len);
}

看起来没什么问题。

整个系统里确实只有一个 Uart1 对象。

Uart1 对象数量 = 1

如果只看设计模式的定义,这个 Singleton 已经实现成功了。

但问题真的解决了吗?

不一定。


一个对象,不代表一个 Owner

假设 send() 底层使用 DMA。

Logger 先发送一条日志:

Logger
  ↓
Uart1::send()
  ↓
配置 DMA

buffer = "motor error..."
length = 14

  ↓
DMA 开始发送

DMA 还没有发送结束,Modem 又过来了:

Modem
  ↓
Uart1::send()
  ↓
重新配置 DMA

buffer = packet
length = 64

最后串口输出就有可能乱掉。

原本希望发送:

motor error...
AA 55 01 02 03 04 ...

实际可能变成:

motor er AA 55 01 02 03 ...

或者出现:

DMA 状态异常
UART Busy
日志缺失
协议数据损坏
发送缓存被覆盖

这个时候有一个很关键的问题:

明明整个程序里只有一个 Uart1 对象,为什么还是会出问题?

因为:

对象只有一个
≠
Owner 只有一个

虽然软件里只有一份 Uart1,但是实际上:

Logger:我可以直接操作 USART1

Modem:我也可以直接操作 USART1

两个模块都认为自己有权控制这个硬件。

系统实际变成了这样:

             USART1
            /      \
       Logger      Modem
         ↑           ↑
       Owner?      Owner?

真正的问题,不是创建了几个 C++ 对象。

而是:

同一个物理资源出现了多个控制者。

嵌入式里真正应该先问的问题

如果 MCU 上真的只有一个 USART1,那么软件设计首先要回答的,不应该是:

Uart 类要不要写成 Singleton?

而应该是:

USART1 到底归谁管?

也就是:

谁才是这个资源唯一的 Owner?

这才是比“对象有几个”更重要的问题。


换一种设计:给 UART 一个真正的 Owner

假设我们规定:

UartTxTask

是 USART1 唯一的管理者。

系统结构改成:

Logger ──────┐
             │
Modem  ──────┼──> TX Queue ──> UartTxTask ──> USART1
             │
Shell  ──────┘

这时候 Logger 不再直接调用:

HAL_UART_Transmit_DMA(...);

Modem 也不能直接配置 USART。

它们只能提交“发送请求”。

例如定义一个消息:

struct TxMessage {
    const uint8_t* data;
    size_t len;
};

Logger 做的事情变成:

void Logger::write(const char* msg)
{
    TxMessage message {
        reinterpret_cast<const uint8_t*>(msg),
        strlen(msg)
    };

    txQueue.push(message);
}

Modem 同样只是提交数据:

void Modem::sendPacket(const uint8_t* data, size_t len)
{
    TxMessage message {
        data,
        len
    };

    txQueue.push(message);
}

真正操作硬件的是 UART Task:

void uartTxTask()
{
    while (true)
    {
        TxMessage message = txQueue.pop();

        uart.send(message.data, message.len);

        waitTxComplete();
    }
}

于是整个数据路径就非常清楚:

Logger
   │
   │
Modem ───────> Queue ───────> UartTxTask ───────> USART1
   │
   │
Shell

只有 UartTxTask 知道怎么操作:

UART 寄存器
DMA
中断
TX 状态机
发送完成事件

这时才真正形成了:

USART1
   ↓
唯一控制入口
   ↓
UartTxTask
   ↓
Single Owner

有意思的地方来了:这时候甚至不需要 Singleton

UART Task 内部完全可以只是一个普通对象:

Uart uart1(USART1);

然后:

void uartTxTask()
{
    while (true)
    {
        TxMessage message = txQueue.pop();

        uart1.send(message.data, message.len);

        waitTxComplete();
    }
}

注意,这里没有:

Uart::getInstance()

也没有:

private:
    Uart();

更没有什么双重检查锁、懒加载。

它就是一个普通对象。

但是整个系统依然只有一个:

Uart uart1

更重要的是:

只有一个模块有权操作它

所以系统仍然非常清晰。

这就是一个很重要的区别:

只有一个实例,不等于必须使用单例模式。

Singleton 和 Single Owner 到底有什么区别?

可以把两种设计放在一起看。

方案一:对象是单例,但谁都可以操作

Logger ─────┐
            │
Modem ──────┼──> Uart::instance() ──> USART1
            │
Shell ──────┘

对象确实只有一个:

Uart 对象 = 1

但是资源的实际控制者有多个:

Logger
Modem
Shell

每个模块都可以随时调用 UART。

于是你还要继续考虑:

谁先调用?
谁后调用?
DMA 正在忙怎么办?
是否需要 mutex?
ISR 会不会同时访问?
发送缓存归谁维护?
状态机由谁维护?

系统仍然很复杂。


方案二:普通对象,但资源只有一个 Owner

Logger ───┐
          │
Modem ────┼──> Queue ──> UartTask ──> Uart uart1 ──> USART1
          │
Shell ────┘

这里没有使用 Singleton Pattern。

但是:

USART1 Owner = UartTask

所有访问都经过它。

结果反而更加稳定。

所以可以简单理解成:

Singleton
关注的是:
“对象到底有几个?”

Single Owner
关注的是:
“这个资源到底谁说了算?”

而在嵌入式系统里,后一个问题通常更加重要。


为什么这件事在嵌入式里特别重要?

因为嵌入式系统里的很多对象,其实都对应真实资源。

例如:

USART1
SPI1
I2C2
ADC1
DMA1
Flash
Watchdog
RTC
PWM Timer
CAN Controller

这些资源有一个共同特点:

它们不是凭空创建的软件概念,而是真实存在于芯片里的硬件。

比如芯片上只有一个:

USART1

那么它对应的:

寄存器
DMA
中断
发送状态
接收状态
buffer
callback

本质上都应该有明确的归属。

如果多个模块分别维护一份状态:

Module A:

uart_busy = true
tx_buffer = buffer_a

而另外一个模块认为:

Module B:

uart_busy = false
tx_buffer = buffer_b

马上就会出现一个问题:

到底谁的状态才是真的?

硬件只有一份:

USART1

软件却出现两套认知:

A 认为 UART 正在忙
B 认为 UART 空闲

这就是非常典型的状态分裂。


单例真正有价值的地方是什么?

如果只从代码形式看,单例好像只是:

限制对象创建数量

但如果再往下一层看,它真正有价值的地方其实是:

多个访问者
    ↓
统一入口
    ↓
统一状态
    ↓
共享资源

放到嵌入式里,可以理解成:

多个业务模块
    ↓
资源管理者
    ↓
UART / SPI / Flash / DMA

所以真正值得学习的,并不是:

private 构造函数怎么写

也不是:

getInstance() 怎么实现

而是:

资源的所有权应该怎么划分

再举一个 SPI 的例子

很多 MCU 上,一个 SPI 总线会挂多个设备:

              SPI1
               │
       ┌───────┼────────┐
       │       │        │
      Flash   IMU     Display

假设:

Flash 使用:

SPI Mode 0
10 MHz

IMU 使用:

SPI Mode 3
8 MHz

Display 使用:

SPI Mode 0
20 MHz

如果三个模块都可以直接操作 SPI:

FlashDriver ──> SPI1

ImuDriver   ──> SPI1

Display     ──> SPI1

很容易出现这样的情况:

IMU 刚刚把 SPI 配置成 Mode 3
            ↓
Display 把 SPI 改成 Mode 0
            ↓
IMU 继续读取数据

这时候 IMU 读到的数据自然可能完全错误。

所以 SPI 真正需要解决的问题,也不是:

Spi 类到底是不是 Singleton

而是:

SPI1 总线到底归谁管

更合理的结构应该是:

Flash ────┐
          │
IMU ──────┼──> SpiBus ───> SPI1
          │
Display ──┘

每个设备只需要告诉 SpiBus:

我要访问哪个设备
我要什么 SPI Mode
我要什么速度
我要发送什么数据

真正负责切换:

CS
Clock
Mode
DMA
Transfer State

的只有 SpiBus。

这样 SPI 总线才真正只有一个 Owner。


嵌入式里的“只有一个”,其实至少有三层含义

很多时候,我们会笼统地说:

这个东西只能有一个

但实际上,这里面至少包含三件不同的事情。


第一层:对象只有一个

例如:

Uart object = 1

这是传统 Singleton Pattern 最关心的问题。


第二层:状态只有一份

例如 UART 的:

tx_busy
rx_busy
tx_buffer
rx_buffer
current_baudrate
dma_state

这些状态最好只有一个可信来源。

也就是:

Single Source of Truth

否则不同模块各维护一套状态,很容易互相矛盾。


第三层:控制者只有一个

只有一个模块真正决定:

什么时候启动 DMA
什么时候停止 DMA
什么时候修改波特率
什么时候清状态位
什么时候切换 SPI Mode
什么时候修改硬件寄存器

这就是:

Single Owner

在嵌入式开发中:

状态只有一份、控制者只有一个,通常比“对象只有一个”更加重要。

一个很实用的判断方法

以后看到一个硬件模块,先不要急着问:

这个类是不是应该做成 Singleton?

先问下面几个问题。


1. 这个资源到底是什么?

先找到真正需要管理的东西。

例如:

USART1
SPI1
Flash
DMA Channel
Watchdog
ADC
PWM Timer

2. 谁拥有它?

最好能明确回答:

UartTask owns USART1

或者:

SpiBus owns SPI1

而不是:

大家都可以用

如果你回答不出“谁拥有它”,通常意味着资源边界还没有设计清楚。


3. 谁允许直接修改它的状态?

例如:

Logger 不能直接启动 UART DMA
Modem 不能直接改 UART 寄存器
Shell 不能直接修改发送状态

它们只能通过 Owner。


4. 其他模块通过什么方式访问它?

可以是:

普通函数调用
消息队列
事件
Command
Request / Response
IPC

具体使用哪种方式并不是重点。

重点是:

访问路径应该明确

5. 资源状态存在哪里?

例如:

class UartManager {
private:
    bool txBusy;
    uint8_t* txBuffer;
    size_t txLength;
};

最好只有一个地方维护这些状态。

否则非常容易出现:

A 认为 Busy
B 认为 Idle

这样的状态分裂。


一个很容易踩的坑:Singleton 不等于线程安全

很多人可能会觉得:

我都已经把 UART 写成 Singleton 了,
应该不会冲突了吧?

其实完全不是一回事。

Singleton 最多只能帮你保证:

对象数量 = 1

它不能保证:

Task A
Task B
ISR

不会同时访问这个对象。

也就是说:

Singleton
    ≠
Thread Safe

进一步说:

Singleton
    ≠
Resource Safe

你仍然可能需要:

mutex
critical section
queue
event
atomic
interrupt protection

所以在 RTOS 环境中,我通常更喜欢这样的设计:

多个 Task
    ↓
Message Queue
    ↓
唯一 Owner Task
    ↓
Hardware

而不是:

多个 Task
    ↓
Singleton
    ↓
mutex
    ↓
Hardware

两种方式都能实现。

但前一种通常更容易把:

硬件状态
DMA 状态
发送队列
错误处理

集中在一个地方。


“只有一个实例”和“必须使用单例模式”是两回事

例如:

Uart uart1(USART1);

Logger logger(uart1);
Modem modem(uart1);

系统里依然只有一个:

uart1

但是 Uart 这个类型本身并没有被设计成 Singleton。

以后如果硬件增加 UART,可以直接:

Uart uart1(USART1);
Uart uart2(USART2);
Uart uart3(USART3);

然后:

Logger logger(uart1);
Gnss gnss(uart2);
Modem modem(uart3);

整个结构依然成立。

这通常比:

Uart::instance()

灵活得多。

因为今天的板子可能只有:

USART1

下一版硬件可能就变成:

USART1 -> Debug
USART2 -> GNSS
USART3 -> LTE

如果一开始就把:

Uart = Singleton

写死了,后面扩展就会比较麻烦。

所以设计时一定要区分:

当前系统只有一个实例

和:

这个类型从设计上永远只能有一个实例

这完全是两回事。


哪些东西比较适合真正做成 Singleton?

嵌入式里有一些东西确实天然接近 Singleton。

比如:

SystemClockManager

WatchdogManager

ResetManager

BootManager

SystemTime

因为这些资源往往具有这样的特点:

物理上天然唯一

+

生命周期基本等于系统生命周期

+

未来几乎没有多实例需求

+

状态需要全局保持一致

这时候使用 Singleton 并没有什么问题。


哪些东西不要轻易写成 Singleton?

下面这些东西最好多想一下:

UART
SPI
I2C
CAN
Sensor
Protocol
CommunicationChannel
BufferPool
StorageBackend

因为它们非常容易从:

一个

变成:

多个

比如今天:

一个 IMU

下一代产品可能变成:

主 IMU
+
备份 IMU

又比如今天:

CAN1

下一代产品可能变成:

CAN1
CAN2

如果代码里到处都是:

CanBus::instance()

后期扩展就会比较痛苦。


回到最开始那句话

现在再来看:

嵌入式里的单例,不要从“对象只能有一个”出发,而要从“资源只能有一个 Owner”出发。

它真正想表达的并不是:

Singleton 不好

也不是:

嵌入式不要使用单例

而是在提醒我们:

不要把注意力只停留在设计模式的代码形式上。

真正应该思考的是:

这个硬件资源是谁的?

谁维护它的状态?

谁能够修改它?

其他模块通过什么方式访问它?

发生并发时由谁协调?

当这些问题都回答清楚之后:

要不要使用 Singleton

反而只是一个实现细节。

可以把整个思路压缩成一句话:

Singleton 管的是对象数量,Single Owner 管的是资源控制权。

而在嵌入式系统里:

真正危险的通常不是多创建了一个对象,而是同一个硬件资源出现了多个控制者。

所以下次再碰到:

UART
SPI
DMA
Flash
ADC
PWM
Watchdog

先不要急着写:

XXX::instance()

先问一句:

这个资源,到底归谁管?

这个问题想明白了,后面的设计通常也就顺了。