第2章 模块边界设计原则:高内聚低耦合、接口隔离、依赖倒置

2.1 基础概念与课程定位

在多设备蓝牙系统中,不同蓝牙协议栈(如 BlueZ、Core Bluetooth、Android BLE)对同一抽象层的实现细节差异巨大。模块边界设计原则决定了替换某一种蓝牙方案时,其他模块是否需要大量修改。本章聚焦三条经典软件工程原则在蓝牙层间的具体落地方式:高内聚低耦合(High Cohesion & Loose Coupling)、接口隔离(Interface Segregation)、依赖倒置(Dependency Inversion)。

2.2 三大原则的角色矩阵

原则 蓝牙场景下的含义 核心收益
高内聚低耦合 每个蓝牙协议栈实现只专注自身职责;上层业务不直接依赖特定实现。 替换协议栈(如 BlueZ → Android BLE)时只改一个模块
接口隔离 将扫描、连接、GATT 操作拆分为独立接口,避免“胖接口”污染模块。 测试时可单独 Mock 某一能力,不影响其他接口
依赖倒置 业务层依赖抽象(蓝牙操作接口),而非具体厂商/平台实现。 框架代码不随平台 SDK 升级而频繁修改

2.3 高内聚低耦合:模块边界的第一道防火墙

高内聚要求一个蓝牙封装模块只完成“与某一种协议栈交互”这一件事;低耦合则要求模块间的依赖链不超过两层。实现时常用适配器模式(Adapter)将平台 API 包在统一外观背后。

实施步骤(以 C++ 蓝牙封装为例)
  1. 定义纯抽象类 BleTransport,声明 Scan()、Connect()、SendData()。
  2. 为每个平台(Linux BlueZ、Android、iOS)编写独立的 BleTransportImpl 子类。
  3. 上层业务只通过 unique_ptr<BleTransport> 或依赖注入容器获取实例。
  4. 编译期通过 CMake 选项或预编译宏选择具体实现。

常见错误:将 BLE 广播解析逻辑与连接管理代码放在同一类中,导致替换协议栈时解析逻辑也需要重写。应通过职责分离将解析器独立为纯数据模块。

// 高内聚的坏例子:一个类做太多事
class BadBleManager {
public:
    void Scan();   // 扫描
    void Connect();// 连接
    int ParseAdvData(uint8_t* raw); // 广播解析混入
};

// 高内聚的好例子:单一职责
class BleScanner { public: void Scan() = 0; };
class BleConnector { public: void Connect() = 0; };
class AdvDataParser { public: int Parse(const uint8_t* raw) = 0; };

2.4 接口隔离:避免“胖接口”污染

蓝牙模块通常需要向业务层暴露:扫描回调、连接状态、GATT 读写、MTU 协商等。如果只用一个接口把所有回调塞进去,会导致业务层即使只关心扫描,也必须实现连接断开等空方法。

接口隔离实现流程

  • 识别变化维度:扫描、连接、数据传输、MTU 协商、错误处理。
  • 每个维度定义一个纯虚接口,只包含该维度的必要方法。
  • 业务层通过多重继承或组合方式选择性实现所需接口。
  • 平台实现类按接口分拆,每个实现类只关心一个维度的细节。
// 隔离后的接口示例(C++)
class IBleScanner {
public:
    virtual void StartScan() = 0;
    virtual void StopScan() = 0;
    virtual void OnDeviceFound(const std::string& addr) = 0;
};

class IBleGattClient {
public:
    virtual void ReadCharacteristic(const std::string& uuid) = 0;
    virtual void WriteCharacteristic(const std::string& uuid, const uint8_t* data) = 0;
};

// 业务控制器只依赖 IBleScanner,而非整个 BleDevice 类
class BleController {
    std::shared_ptr<IBleScanner> scanner;
public:
    explicit BleController(std::shared_ptr<IBleScanner> s) : scanner(s) {}
    void StartSearch() { scanner->StartScan(); }
};
调试提示:在 Android/iOS 端实现隔离接口时,可先在本地创建一个“Mock 平台实现”,输出日志并验证接口调用链是否正确,再替换为真实硬件 SDK。这有助于尽早发现接口遗漏或参数不匹配问题。

2.5 依赖倒置:让框架不依赖平台

依赖倒置原则要求:抽象不应依赖细节,细节应依赖抽象。在蓝牙模块中这意味着:业务层只依赖 IBleScanner、IBleConnector 等抽象接口;具体平台实现(如 Linux BlueZ、Android BLE)作为插件注入主程序。

实现流程

  1. 定义一个抽象工厂(Abstract Factory)IBleFactory,声明 CreateScanner()、CreateConnector()。
  2. 为每个平台实现具体工厂(如 BlueZBleFactory、AndroidBleFactory)。
  3. 主程序启动时根据运行时检测到的平台(或编译时宏)选择工厂。
  4. 所有业务代码只通过工厂获得接口指针,不直接实例化任何平台类。
// 抽象工厂与依赖倒置
// 业务层完全不感知平台类型
class BleApp {
    std::unique_ptr<IBleScanner> scanner;
    std::unique_ptr<IBleConnector> connector;
public:
    BleApp(std::unique_ptr<IBleFactory> factory) {
        scanner = factory->CreateScanner();
        connector = factory->CreateConnector();
    }
    void Run() { scanner->StartScan(); }
};

// 平台实现:Linux BlueZ
class BlueZBleFactory : public IBleFactory {
public:
    std::unique_ptr<IBleScanner> CreateScanner() override {
        return std::make_unique<BlueZScanner>();
    }
    std::unique_ptr<IBleConnector> CreateConnector() override {
        return std::make_unique<BlueZConnector>();
    }
};
常见错误:在抽象工厂方法中返回具体平台类(如 return new BlueZScanner()),但整个函数签名中暴露了 BlueZScanner 类型。必须确保工厂返回的是接口指针 IBleScanner*,避免编译依赖传播。

2.6 整体架构与内联 SVG 关系图

模块边界设计原则关系图 模块边界设计原则支柱 高内聚低耦合为结构基础,接口隔离与依赖倒置共同保证可替换性 高内聚低耦合 职责单一·依赖链短 接口隔离 分维接口·避免胖接口 依赖倒置 抽象不依赖细节 具体实现层(平台适配器) BlueZScanner | CoreBluetoothManager | AndroidBleGatt 遵循高内聚:每个适配器只处理一种平台的完整协议栈 设计效果 • 替换蓝牙方案时只改适配器层 • 业务测试时可轻松 Mock 任意接口 • 新增平台不会污染已有代码 违反原则的后果 • 修改一处实现导致全模块重构 • 依赖链混乱,构建时头文件冲突 • 无法针对单一能力编写单元测试

2.7 性能与稳定性优化

原则 优化方向 典型做法
高内聚低耦合 减少跨模块调用层级 将扫描回调直接注册到业务线程,避免中间代理类
接口隔离 降低接口虚拟调用次数 在热路径(如 GATT notification)采用模板策略模式代替虚函数
依赖倒置 减少编译期模板膨胀 使用 Pimpl 惯用法隐藏平台实现,避免头文件过度依赖

2.8 部署与编译期注意事项

在 CMake 中使用 INTERFACE 目标定义抽象接口库,使业务代码只在编译时依赖接口头文件,运行时注入具体实现:

# CMakeLists.txt 示例(伪目标) 
add_library(ble_interfaces INTERFACE)
target_include_directories(ble_interfaces INTERFACE interfaces/)

add_library(ble_impl_bluez SHARED src/bluez_scanner.cpp src/bluez_connector.cpp)
target_link_libraries(ble_impl_bluez PUBLIC ble_interfaces)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE ble_interfaces)
# 运行时通过 dlopen 或 -rdynamic + 符号覆盖选择具体的库
调试技巧:在 Android Studio 或 Xcode 中为每个平台实现创建独立的 Build Target,通过 Scheme/Configuration 切换,验证接口隔离是否完全 —— 编译时如果出现“缺少符号”意味着某接口未被完全实现。

2.9 常见错误排查清单

  • 错误:接口隔离后出现大量空实现 —— 应当合并为纯虚抽象类,避免每个业务都实现所有虚函数。
  • 错误:依赖倒置工厂返回原始指针导致生命周期混乱 —— 使用 std::shared_ptr 或 unique_ptr 并明确所有权。
  • 错误:高内聚模块内包含了日志、统计等横切关注点 —— 应将日志通过依赖注入单独传递,而非硬编码在模块内部。
  • 错误:在头文件中暴露了平台相关的 #include <Bluetooth/...> —— 使用前置声明和 Pimpl 消除编译依赖。