第三章:工厂方法模式——硬件驱动的“生产流水线”
说实话,做嵌入式这些年,我踩过最大的坑之一,就是驱动代码的“硬耦合”。
你想想看,项目初期芯片选型还没定,驱动代码就得先写着。结果呢?换一个传感器,改一堆代码。换一个Flash型号,又改一堆。改到最后,连自己都分不清哪个函数对应哪个硬件了。
嗯,工厂方法模式就是来解决这个问题的。
3.1 什么是工厂方法模式?
说白了,工厂方法模式就是定义一个创建对象的接口,让子类决定实例化哪个类。在嵌入式世界里,这个“对象”通常就是硬件驱动实例。
我个人的习惯是,把驱动创建的逻辑从业务代码中抽出来。业务代码只管“我要一个SPI Flash驱动”,至于这个驱动是W25Q64还是GD25Q32,由工厂去决定。
核心思想: 将对象的创建和使用分离。客户端不需要知道具体驱动类的细节,只需要知道工厂接口。
3.2 为什么嵌入式需要工厂方法?
我在项目中遇到过这样一个场景:一个产品需要支持两种不同的温湿度传感器——SHT30和DHT22。硬件设计时留了兼容接口,但软件怎么办?
如果用传统方式,代码里到处都是:
// 糟糕的设计
#if defined(USE_SHT30)
sht30_init();
sht30_read(&data);
#elif defined(USE_DHT22)
dht22_init();
dht22_read(&data);
#endif
这种写法,每次换传感器都要重新编译整个工程。而且,如果后期要同时支持两种传感器呢?代码就乱成一锅粥了。
工厂方法模式就能优雅地解决这个问题。
3.3 工厂方法模式的结构
先看一个典型的UML结构(我习惯用C语言实现,但思想是一样的):
| 角色 | 说明 | 嵌入式中的例子 |
|---|---|---|
| 抽象产品 | 定义驱动接口 | SensorDriver(包含init、read、deinit) |
| 具体产品 | 具体硬件驱动实现 | SHT30Driver、DHT22Driver |
| 抽象工厂 | 定义创建驱动的方法 | SensorFactory(createDriver) |
| 具体工厂 | 实现具体驱动的创建 | SHT30Factory、DHT22Factory |
你可能会问:“这不就是多态吗?” 没错,工厂方法模式本质上就是利用多态来实现对象的延迟创建。但在C语言里,我们得用函数指针来模拟。
3.4 C语言实现示例
来看一个我实际用过的代码片段。这是抽象驱动接口的定义:
// driver_interface.h
typedef struct {
int (*init)(void);
int (*read)(uint8_t *buf, uint32_t len);
int (*deinit)(void);
} driver_ops_t;
然后定义工厂接口:
// factory_interface.h
typedef struct {
driver_ops_t* (*create)(void);
void (*destroy)(driver_ops_t *drv);
} factory_ops_t;
具体工厂实现——比如SHT30的工厂:
// sht30_factory.c
static driver_ops_t sht30_driver = {
.init = sht30_init,
.read = sht30_read,
.deinit = sht30_deinit
};
static factory_ops_t sht30_factory = {
.create = sht30_create,
.destroy = sht30_destroy
};
driver_ops_t* sht30_create(void) {
// 这里可以做些初始化检查
return &sht30_driver;
}
客户端代码就干净多了:
// main.c
void app_main(void) {
factory_ops_t *factory = get_sensor_factory(SENSOR_TYPE_SHT30);
driver_ops_t *drv = factory->create();
drv->init();
drv->read(buffer, sizeof(buffer));
drv->deinit();
factory->destroy(drv);
}
我的经验: 在资源受限的MCU上,我通常把工厂实例做成静态分配,避免动态内存分配带来的碎片问题。上面的例子就是静态实例,简单可靠。
3.5 在Bootloader中的应用
Bootloader是工厂方法模式大显身手的地方。为什么?因为Bootloader需要支持多种硬件配置。
我记得有一次做OTA升级的Bootloader,需要支持三种不同的Flash芯片:
- W25Q64(SPI接口)
- MX25L128(QSPI接口)
- AT45DB161(SPI接口,但页大小不同)
如果用工厂方法模式,Bootloader启动时先读取硬件ID,然后通过工厂创建对应的Flash驱动。核心代码完全不用改。
// bootloader中的工厂使用
void bootloader_main(void) {
uint32_t flash_id = read_hardware_id();
factory_ops_t *flash_factory = get_flash_factory(flash_id);
if (flash_factory == NULL) {
// 不支持的Flash,进入错误处理
error_handler();
return;
}
driver_ops_t *flash = flash_factory->create();
flash->init();
// 执行升级流程
perform_ota_update(flash);
flash->deinit();
flash_factory->destroy(flash);
}
避坑指南: 我曾经在Bootloader里直接用条件编译来选择Flash驱动,结果生产时发现有一批板子用了不同型号的Flash。因为Bootloader已经烧死了,只能返工。从那以后,我所有的Bootloader都改用工厂方法模式,运行时动态选择驱动。
3.6 工厂方法模式的优缺点
| 优点 | 缺点 |
|---|---|
| 解耦对象创建和使用,代码更灵活 | 增加了类的数量,代码量会多一些 |
| 符合开闭原则,新增驱动不用改现有代码 | 对于简单场景,可能过度设计 |
| 便于单元测试,可以注入Mock驱动 | 在C语言中实现需要函数指针,可读性略差 |
| 运行时动态选择驱动,适应多硬件版本 | 对资源有限的MCU,函数指针调用有微小开销 |
3.7 什么时候该用?什么时候不该用?
我个人总结了几条经验:
- 该用: 产品需要支持多种同类型硬件(多种Flash、多种传感器、多种显示面板)
- 该用: Bootloader或固件需要兼容不同硬件版本
- 该用: 驱动代码需要独立测试,或者需要模拟硬件进行开发
- 不该用: 整个项目只有一种硬件,且永远不会变
- 不该用: MCU资源极度紧张(比如只有2KB RAM),函数指针的开销都承受不起
一句话总结: 工厂方法模式就是给硬件驱动加了一层“调度中心”。你告诉它你要什么,它给你造出来。至于怎么造的,你不用管。
嗯,这一章就到这里。下一章我们会讲抽象工厂模式——当你有多个相互关联的硬件需要一起创建时,就该它上场了。