4、轻量级DI容器:嵌入式DI容器设计、注册表模式、工厂模式结合DI

好,咱们进入第四章。前面几章我们把依赖注入的概念、手动注入的写法都过了一遍。你可能会想:每次手动new对象、传指针,代码量不小,而且耦合还是没完全解开。有没有一个东西,能帮我们统一管理这些依赖?

有的。这就是轻量级的DI容器。

我刚开始做嵌入式项目时,也觉得容器是Java、C#那些大系统才用的东西。后来在一个多传感器融合的项目里,模块间依赖关系越来越乱,手动初始化代码写了上千行,改一个接口要牵连七八个文件。嗯,那时候我才意识到——嵌入式也需要一个容器,只不过要足够轻。

4.1 嵌入式DI容器的设计原则

嵌入式环境资源有限,我们不能把Spring那套搬过来。我总结了几条核心原则:

  • 静态注册为主:运行时反射在C语言里不现实,我们用编译期或启动时的静态注册。
  • 零动态分配:容器内部不使用malloc,所有对象池在编译期确定大小。
  • 接口与实现分离:容器只管理接口指针,具体实现由注册表决定。
  • 可裁剪:用宏控制功能开关,不用到的特性不占ROM。

核心思路:DI容器本质上是一个“对象工厂 + 依赖关系表”。你告诉容器“我需要一个I2C驱动”,容器查表,返回给你注册好的实例。

4.2 注册表模式:容器的心脏

注册表模式,说白了就是一张全局的“接口-实现”映射表。每个接口类型对应一个创建函数指针,容器启动时把所有实现注册进去。

我在一个车载项目里用过这种设计。当时有3种不同的温度传感器,每种驱动都实现了同一个temp_sensor_if接口。通过注册表,业务代码根本不知道具体用的是哪款芯片。

4.2.1 注册表的数据结构

// 接口类型枚举
typedef enum {
    IFACE_I2C_MASTER,
    IFACE_SPI_MASTER,
    IFACE_GPIO_CTRL,
    IFACE_TIMER,
    IFACE_COUNT  // 总接口数
} iface_type_t;

// 注册表条目
typedef struct {
    iface_type_t type;
    void* (*creator)(void);   // 创建实例的函数指针
    void* instance;           // 缓存单例实例
    uint8_t flags;            // 标志位:单例/多例、是否已初始化
} registry_entry_t;

// 注册表本身
static registry_entry_t g_registry[IFACE_COUNT];

我的习惯:把IFACE_COUNT放在枚举最后,这样数组大小自动跟着接口数量走,加新接口时不容易漏掉注册表条目。

4.2.2 注册与查找

// 注册一个实现
void di_register(iface_type_t type, void* (*creator)(void), uint8_t flags) {
    g_registry[type].type    = type;
    g_registry[type].creator = creator;
    g_registry[type].instance = NULL;
    g_registry[type].flags   = flags;
}

// 获取接口实例
void* di_resolve(iface_type_t type) {
    registry_entry_t* entry = &g_registry[type];
    
    if (entry->flags & FLAG_SINGLETON) {
        // 单例模式:第一次创建,后续复用
        if (entry->instance == NULL) {
            entry->instance = entry->creator();
        }
        return entry->instance;
    } else {
        // 多例模式:每次都创建新实例
        return entry->creator();
    }
}

你看,代码量很少。但就是这个简单的表,把模块间的直接依赖彻底解开了。

4.3 工厂模式结合DI

注册表解决了“接口到实现”的映射。但有时候,一个接口可能有多个实现,或者实现需要额外参数。这时候工厂模式就派上用场了。

工厂模式在DI容器里扮演的角色是:封装对象的创建逻辑。容器不关心对象怎么造出来的,只关心工厂函数长什么样。

4.3.1 带参数的工厂

// 带参数的工厂函数类型
typedef void* (*param_creator_t)(void* param);

// 扩展的注册表条目
typedef struct {
    iface_type_t     type;
    param_creator_t  creator;
    void*            create_param;   // 创建参数
    void*            instance;
    uint8_t          flags;
} ext_registry_entry_t;

// 示例:I2C设备工厂,参数是设备地址
void* i2c_device_factory(void* param) {
    uint8_t addr = *(uint8_t*)param;
    return i2c_device_create(addr);
}

// 注册时传入参数
uint8_t dev_addr = 0x50;
di_register_ext(IFACE_EEPROM, i2c_device_factory, &dev_addr, FLAG_SINGLETON);

注意:传入工厂的参数必须是全局或静态变量,不能是局部变量。否则注册完成后参数就失效了。我曾经在这个坑里栽过跟头,排查了半天才发现是栈上的参数被覆盖了。

4.3.2 工厂模式与策略模式组合

有时候,同一个接口在不同场景下需要不同的实现。比如日志模块:调试阶段用串口输出,量产阶段用Flash存储。工厂模式可以帮我们做运行时切换。

// 日志接口
typedef struct {
    void (*log)(const char* msg);
} logger_iface_t;

// 两个实现
static logger_iface_t serial_logger = { serial_log };
static logger_iface_t flash_logger  = { flash_log };

// 工厂函数:根据模式返回不同实现
void* logger_factory(void* param) {
    uint8_t mode = *(uint8_t*)param;
    if (mode == LOG_MODE_DEBUG) {
        return &serial_logger;
    } else {
        return &flash_logger;
    }
}

这样做的好处是:业务代码只依赖logger_iface_t接口,切换实现时一行代码都不用改。

4.4 整体架构:DI容器内部结构

下面这张图展示了DI容器的核心工作流程。我建议你仔细看看,理解数据是怎么流转的。

嵌入式轻量级DI容器架构图 应用层 业务模块A 业务模块B 业务模块C 调用 di_resolve() 获取接口实例 DI容器核心 注册表 类型 | 工厂指针 | 实例 | 标志 [I2C] [spi_create] [0x00] [S] [SPI] [spi_create] [0x00] [S] 工厂管理器 单例工厂 | 多例工厂 参数化工厂 生命周期管理 初始化 | 销毁 | 重置 引用计数 驱动/实现层 I2C驱动 SPI驱动 GPIO驱动 定时器驱动 实现接口函数 注册到容器 流程:应用层调用 di_resolve() → 容器查注册表 → 工厂创建实例 → 返回给应用层

4.5 实战:构建一个微型DI容器

光说不练假把式。咱们动手写一个完整的微型DI容器,用在STM32或类似平台上。

4.5.1 容器头文件

// di_container.h
#ifndef DI_CONTAINER_H
#define DI_CONTAINER_H

#include <stdint.h>

// 接口ID定义
typedef enum {
    DI_I2C_MASTER,
    DI_SPI_MASTER,
    DI_UART,
    DI_GPIO,
    DI_TIMER,
    DI_LOG,
    DI_MEMORY,
    DI_SENSOR_TEMP,
    DI_SENSOR_HUMI,
    DI_MAX_INTERFACES
} di_iface_t;

// 容器初始化
void di_init(void);

// 注册接口实现
void di_register(di_iface_t iface, void* (*factory)(void));

// 带参数的注册
void di_register_param(di_iface_t iface, void* (*factory)(void*), void* param);

// 解析接口实例
void* di_resolve(di_iface_t iface);

// 重置容器(用于单元测试)
void di_reset(void);

#endif

4.5.2 容器实现

// di_container.c
#include "di_container.h"
#include <string.h>

#define FLAG_REGISTERED  (1 << 0)
#define FLAG_SINGLETON   (1 << 1)
#define FLAG_PARAM       (1 << 2)

typedef struct {
    di_iface_t iface;
    void* (*factory)(void);
    void* (*param_factory)(void*);
    void* param;
    void* instance;
    uint8_t flags;
} container_entry_t;

static container_entry_t s_entries[DI_MAX_INTERFACES];
static uint8_t s_initialized = 0;

void di_init(void) {
    memset(s_entries, 0, sizeof(s_entries));
    s_initialized = 1;
}

void di_register(di_iface_t iface, void* (*factory)(void)) {
    if (!s_initialized || iface >= DI_MAX_INTERFACES) return;
    
    s_entries[iface].iface  = iface;
    s_entries[iface].factory = factory;
    s_entries[iface].flags  = FLAG_REGISTERED | FLAG_SINGLETON;
}

void di_register_param(di_iface_t iface, void* (*factory)(void*), void* param) {
    if (!s_initialized || iface >= DI_MAX_INTERFACES) return;
    
    s_entries[iface].iface        = iface;
    s_entries[iface].param_factory = factory;
    s_entries[iface].param        = param;
    s_entries[iface].flags        = FLAG_REGISTERED | FLAG_SINGLETON | FLAG_PARAM;
}

void* di_resolve(di_iface_t iface) {
    if (!s_initialized || iface >= DI_MAX_INTERFACES) return NULL;
    
    container_entry_t* entry = &s_entries[iface];
    
    if (!(entry->flags & FLAG_REGISTERED)) {
        return NULL;  // 未注册
    }
    
    if (entry->instance == NULL) {
        if (entry->flags & FLAG_PARAM) {
            entry->instance = entry->param_factory(entry->param);
        } else {
            entry->instance = entry->factory();
        }
    }
    
    return entry->instance;
}

void di_reset(void) {
    for (int i = 0; i < DI_MAX_INTERFACES; i++) {
        s_entries[i].instance = NULL;
    }
}

使用建议:在单元测试中,每次测试前调用di_reset(),然后注册Mock实现。这样测试之间不会互相影响。我在一个传感器驱动测试套件里就是这么做的,覆盖率从60%提到了92%。

4.6 注册表模式 vs 工厂模式 vs 纯DI

这三种模式经常被放在一起比较。我整理了一张表,帮你快速区分:

模式 核心职责 适用场景 资源开销
注册表模式 维护接口-实现映射表 接口数量固定,实现单一 低(数组+指针)
工厂模式 封装对象创建逻辑 创建过程复杂,需要参数 中(函数指针+参数)
纯DI 手动注入依赖 小型项目,接口少 最低(无额外开销)

实际项目中,我通常把三者结合起来:注册表做容器骨架,工厂模式处理复杂对象的创建,纯DI用在性能敏感的路径上。

4.7 避坑指南

做DI容器时,有几个坑我踩过,你注意避开:

  • 循环依赖:A依赖B,B又依赖A。容器解析时会死循环。解决办法:加一个解析深度计数器,超过阈值就报错。
  • 线程安全:RTOS环境下,多个任务同时调用di_resolve可能出问题。加一个简单的互斥锁,或者用临界区保护。
  • 内存泄漏:多例模式下每次创建新对象,但没人负责销毁。我建议多例对象都实现一个destroy函数,容器在重置时统一清理。
  • 接口ID冲突:多人协作时,枚举值可能重复。用静态断言检查DI_MAX_INTERFACES是否等于实际注册数。

我曾经...在一个项目里忘了处理循环依赖,结果系统启动时直接卡死在di_resolve里。用调试器一看,调用栈深到离谱。从那以后,我每个容器都加了深度检测,超过10层就触发断言。

4.8 小结

轻量级DI容器,说白了就是一张注册表加上几个工厂函数。它不复杂,但能帮你把模块间的耦合降到最低。嵌入式环境里,我们不需要Spring那样的庞然大物,一个几百行的C文件就足够了。

记住:容器的目的是让代码更容易测试、更容易替换、更容易维护。如果它反而让你的代码变复杂了,那说明你用错了。