第三章:工厂方法模式——硬件驱动的“生产流水线”

说实话,做嵌入式这些年,我踩过最大的坑之一,就是驱动代码的“硬耦合”。

你想想看,项目初期芯片选型还没定,驱动代码就得先写着。结果呢?换一个传感器,改一堆代码。换一个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),函数指针的开销都承受不起

一句话总结: 工厂方法模式就是给硬件驱动加了一层“调度中心”。你告诉它你要什么,它给你造出来。至于怎么造的,你不用管。

嗯,这一章就到这里。下一章我们会讲抽象工厂模式——当你有多个相互关联的硬件需要一起创建时,就该它上场了。