标签 gcms 下的文章

用嵌入式 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 理解成一个特殊的地方:

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

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

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