用嵌入式 C 重新理解工厂模式:它解决的从来不只是“创建对象”
用嵌入式 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 HAL1. 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 I2C4. 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 理解成一个特殊的地方:
它知道整个系统是怎么组装起来的。
而其他模块,只需要知道自己的接口、自己的职责,以及自己依赖什么。
当代码能做到这一点时,工厂模式就不再只是一个“设计模式题目”,而是真正开始参与嵌入式软件架构设计了。