第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++ 蓝牙封装为例)
- 定义纯抽象类
BleTransport,声明Scan()、Connect()、SendData()。 - 为每个平台(Linux BlueZ、Android、iOS)编写独立的
BleTransportImpl子类。 - 上层业务只通过
unique_ptr<BleTransport>或依赖注入容器获取实例。 - 编译期通过 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)作为插件注入主程序。
实现流程
- 定义一个抽象工厂(Abstract Factory)
IBleFactory,声明CreateScanner()、CreateConnector()。 - 为每个平台实现具体工厂(如
BlueZBleFactory、AndroidBleFactory)。 - 主程序启动时根据运行时检测到的平台(或编译时宏)选择工厂。
- 所有业务代码只通过工厂获得接口指针,不直接实例化任何平台类。
// 抽象工厂与依赖倒置
// 业务层完全不感知平台类型
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 关系图
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 消除编译依赖。