第3章 模块边界定义原则

在BLE模块中,依赖注入边界的选择直接决定系统是否易于扩展、测试与维护。本章围绕单一职责、接口隔离、依赖倒置三项原则,结合BLE实际场景分析其应用方式,并给出边界粒度的选型权衡。

3.1 单一职责原则(SRP)在BLE边界中的映射

每个模块只应有一个改变的理由。在BLE系统中,典型的职责拆分包括:

  • 广播管理:负责广播参数的配置与启停,不涉及连接控制。
  • 连接管理:处理连接建立、参数更新、断开事件,不关心广播内容。
  • GATT服务实现:提供服务注册、特征读写回调,与底层HCI解耦。
  • 安全配对:单独封装SM(安全管理)协议逻辑。
若一个模块同时处理广播参数和连接参数,则广播协议变更会影响连接逻辑,违反SRP。建议按协议层+功能域交叉划分边界。

3.2 接口隔离原则(ISP)——最小依赖接口

客户端不应依赖它不需要的接口。在DI边界设计时,应针对不同消费者(广播模块、连接模块、GATT服务器)提供粒度精细的接口:

消费者需要的方法不应暴露的方法
广播模块startAdvertising, stopAdvertising, setAdvertisementDataconnect, disconnect, discoverServices
连接模块connect, disconnect, updateConnectionParamsstartAdvertising, writeCharacteristic
GATT服务器registerService, handleRead, handleWriteadvertise, scan

通过将IBleRadio拆分为IAdvertiser、IConnector、IGattServer三个接口,各模块只依赖自己需要的API,降低变更影响面。

// 反例:臃肿的单一接口
struct IBleController {
    virtual void startAdvertising() = 0;
    virtual void stopAdvertising() = 0;
    virtual void connect(const Address& addr) = 0;
    virtual void disconnect() = 0;
    virtual void registerService(GattService& svc) = 0;
    // ……更多方法
};

// 正例:按职责拆分的接口
struct IAdvertiser {
    virtual void start() = 0;
    virtual void stop() = 0;
};
struct IConnector {
    virtual void connect(const Address& addr) = 0;
    virtual void disconnect() = 0;
};
struct IGattServer {
    virtual void registerService(GattService& svc) = 0;
    virtual void onRead(const Request& req) = 0;
};

3.3 依赖倒置原则(DIP)——抽象边界

高层模块不应依赖低层模块,二者都应依赖抽象。在BLE DI边界中,应用层(如心率计Profile)应依赖抽象的GATT服务接口,而非具体的芯片驱动。底层驱动实现该接口,并通过DI容器注入。

依赖倒置使BLE协议栈可轻松切换芯片(如从Nordic nRF52832切换到Dialog DA14695),只需提供新的驱动实现类,应用层代码不变。
// 抽象接口定义在高层模块
class HeartRateServiceInterface {
public:
    virtual void updateHeartRate(uint8_t value) = 0;
    virtual ~HeartRateServiceInterface() = default;
};

// 具体实现位于底层芯片适配层
class Nrf52HeartRateService : public HeartRateServiceInterface {
    // 使用nRF SDK的API实现
};

// DI容器注册
// di_container.register<HeartRateServiceInterface, Nrf52HeartRateService>();

3.4 边界粒度选择

过粗的边界(如单个“BleManager”模块)会导致内部耦合爆炸;过细的边界(每个函数一个模块)则引入大量接口与组装成本。BLE场景下推荐两种粒度:

  • 按协议层粒度:HCI、L2CAP、GATT、GAP各为一模块。优点是与标准分层对齐,适合需要深度定制协议的团队。缺点是上层业务常需跨层调用,产生“横切”依赖。
  • 按功能域粒度:广播、扫描、连接、配对、数据传输。优点是与业务场景自然对应,上层应用直接感知。缺点是需要额外协调层处理跨域事件(如连接成功后的广播停止)。
粒度类型优点缺点适用场景
协议层粒度技术边界清晰,分层测试容易业务代码需跨层拼装BLE协议栈研发、芯片SDK开发
功能域粒度面向业务,API直观可能出现职责重叠(如扫描与广播共用射频)终端产品应用层开发
风险点:若在同一个系统中混用两种粒度,会造成边界混乱,DI容器需要同时处理多层抽象,增加配置复杂度。建议整个项目统一选择一种粒度,并在架构文档中显式约定。

3.5 依赖关系与数据流

下图展示应用层通过抽象接口(DIP)与具体驱动解耦,DI容器负责组装依赖关系。数据流方向为:应用层 → DI容器 → 具体驱动实现 → 硬件寄存器。

依赖倒置原则下的BLE模块边界结构 Application(心率计) HeartRateServiceInterface DI Container Nrf52HeartRateService nRF52832 硬件寄存器 Dialog DA14695 硬件寄存器 高层模块(Application)仅依赖抽象接口,具体实现通过DI容器注入,可无缝切换BLE芯片

3.6 落地步骤与验收标准

  1. 职责识别:列出BLE系统中所有功能点,按SRP分组,每组形成一个模块候选。
  2. 接口裁剪:为每个模块定义最小接口集,确保ISP满足——使用接口隔离工具类或C++纯虚类。
  3. 抽象提取:将模块接口提升至独立头文件(抽象层),具体实现仅在底层编译单元包含。
  4. DI容器引入:选择轻量级DI框架(如Boost.DI或自建工厂),将抽象-具体绑定注册。示例配置:
// 使用工厂函数实现简单DI绑定
auto createHeartRateService = [](const BoardConfig& cfg) -> std::shared_ptr<HeartRateServiceInterface> {
    if (cfg.chip == ChipType::NRF52) {
        return std::make_shared<Nrf52HeartRateService>(cfg.spi);
    } else if (cfg.chip == ChipType::DA14695) {
        return std::make_shared<Da14695HeartRateService>(cfg.i2c);
    }
    throw std::runtime_error("unsupported chip");
};
  1. 验收标准:
  2. 新增芯片实现时,仅需添加新的实现类并在DI容器注册,应用层代码0修改(DIP验证)。
  3. 修改广播模块时不需重新编译连接模块(SRP验证)。
  4. 连接模块引用的接口只包含connect/disconnect/updateParams,不存在startAdvertising方法(ISP验证)。
边界粒度的最终验证:在单元测试中,可用Mock对象替换任一模块,测试其他模块是否依然能正常构建与运行。若频繁出现“我不得不Mock掉整个BleStack才能测试GATT服务”,则说明边界粒度过粗,需要进一步拆分。