概述:策略模式是连接模块的骨架
策略模式(Strategy Pattern)是分布式软总线连接模块的第一架构模式——它决定了整个模块的骨架结构。简单来说,策略模式解决的核心问题是:同一件事有多种做法,需要在运行时灵活选择用哪种做法。
在连接模块中,"连接设备"这件事有三种做法——通过蓝牙BR、通过蓝牙BLE、通过TCP,将来还可能新增WiFi Direct等。策略模式让连接管理器不需要知道每种连接具体怎么实现,只需要定义一套统一的"接口契约",然后让各连接类型各自实现。调用时,根据连接类型自动路由到正确的实现。
这就好比你去餐厅点餐:菜单(ConnectFuncInterface)定义了"炒菜"这个动作,但后厨有川菜师傅(BR)、粤菜师傅(BLE)、鲁菜师傅(TCP),你点什么口味,就派给对应的师傅来做——你不需要亲自跑到后厨去指定。
一、策略模式原理详解
1.1 模式定义
策略模式(Strategy Pattern),又称政策模式(Policy Pattern),属于GoF 23种设计模式中的行为型模式。
核心思想:定义一组算法(策略),将每一个算法封装到具有共同接口的独立类中,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。
用大白话说就是:把"做什么"和"怎么做"分开。"做什么"由上下文(Context)决定,"怎么做"由具体策略(Strategy)决定,两者通过接口解耦。
1.2 三个核心角色
| 角色 | 英文名 | 中文解释 | 职责 |
|---|---|---|---|
| 策略接口 | Strategy(策略/抽象策略) | 定义所有支持的算法的公共接口 | 声明统一的方法签名,是所有具体策略的契约 |
| 具体策略 | ConcreteStrategy(具体策略) | 实现策略接口中定义的算法 | 每个具体策略封装一种具体的算法实现 |
| 上下文 | Context(环境/上下文) | 持有策略引用,将请求委托给策略 | 维护对策略对象的引用,在运行时切换策略 |
1.3 经典UML结构
1.4 经典代码骨架(伪代码)
// 1. 策略接口:定义统一契约 typedef struct { int32_t (*algorithm)(int32_t input); // 算法接口 } Strategy; // 2. 具体策略A:算法A的实现 int32_t AlgorithmA(int32_t input) { return input * 2; } Strategy strategyA = { .algorithm = AlgorithmA }; // 3. 具体策略B:算法B的实现 int32_t AlgorithmB(int32_t input) { return input + 10; } Strategy strategyB = { .algorithm = AlgorithmB }; // 4. 上下文:持有策略引用,运行时选择 Strategy *currentStrategy; void setStrategy(Strategy *s) { currentStrategy = s; } int32_t doSomething(int32_t input) { return currentStrategy->algorithm(input); } // 5. 使用 setStrategy(&strategyA); // 选择策略A result = doSomething(5); // 调用 → 5 * 2 = 10 setStrategy(&strategyB); // 切换到策略B result = doSomething(5); // 调用 → 5 + 10 = 151.5 策略模式的核心价值
| 价值点 | 说明 |
|---|---|
| 消除条件分支 | 不用写一堆if-else或switch-case,用策略替换直接分发 |
| 算法独立变化 | 新增算法不影响已有算法和上下文 |
| 运行时切换 | 可以在程序运行过程中动态更换策略 |
| 复用与组合 | 同一策略可被多个上下文复用 |
二、连接模块中策略模式的完整实现
2.1 角色映射:三个角色如何对应
| 经典角色 | 连接模块对应 | 具体代码 |
|---|---|---|
| Strategy(策略接口) | ConnectFuncInterface | softbus_conn_manager.h |
| ConcreteStrategy(具体策略) | BR策略、BLE策略、TCP策略 | ConnInitBr()、ConnInitBle()、ConnInitTcp()返回的对象 |
| Context(上下文) | ConnManager(连接管理器) | g_connManager[]数组 + 路由分发函数 |
2.2 策略接口:ConnectFuncInterface
这是整个策略模式的灵魂——它定义了"连接"这件事到底包含哪些动作。所有连接类型必须实现这套接口:
typedef struct { int32_t (*ConnectDevice)(const ConnectOption *option, uint32_t requestId, const ConnectResult *result); int32_t (*PostBytes)(uint32_t connectionId, uint8_t *data, uint32_t len, ...); int32_t (*DisconnectDevice)(uint32_t connectionId); int32_t (*DisconnectDeviceNow)(const ConnectOption *option); int32_t (*GetConnectionInfo)(uint32_t connectionId, ConnectionInfo *info); int32_t (*StartLocalListening)(const LocalListenerInfo *info); int32_t (*StopLocalListening)(const LocalListenerInfo *info); bool (*CheckActiveConnection)(const ConnectOption *info, bool needOccupy); int32_t (*UpdateConnection)(uint32_t connectionId, UpdateOption *option); int32_t (*PreventConnection)(const ConnectOption *option, uint32_t time); int32_t (*ConfigPostLimit)(const LimitConfiguration *configuration); } ConnectFuncInterface;用大白话翻译每个方法:
| 方法 | 含义 | 生活类比 |
|---|---|---|
ConnectDevice | 建立连接 | 拨打电话 |
PostBytes | 发送数据 | 说话 |
DisconnectDevice | 断开连接(优雅) | 说再见后挂电话 |
DisconnectDeviceNow | 立即断开连接 | 直接挂电话 |
GetConnectionInfo | 查询连接信息 | 查看通话记录 |
StartLocalListening | 开始监听 | 打开电话等待来电 |
StopLocalListening | 停止监听 | 关闭电话不再接听 |
CheckActiveConnection | 检查是否有活跃连接 | 查看是否在通话中 |
UpdateConnection | 更新连接参数 | 通话中调整音量 |
PreventConnection | 阻止连接 | 设置免打扰 |
ConfigPostLimit | 配置流量限制 | 设置流量套餐 |
2.3 具体策略:三种连接实现
BR策略(蓝牙经典连接)
在 softbus_conn_br_manager.c:1856 中填充:
static ConnectFuncInterface connectFuncInterface = { .ConnectDevice = BrConnectDevice, // BR方式建立连接 .PostBytes = ConnBrPostBytes, // BR方式发送数据 .DisconnectDevice = BrDisconnectDevice, // BR方式优雅断连 .DisconnectDeviceNow = BrDisconnectDeviceNow, // BR方式立即断连 .GetConnectionInfo = BrGetConnectionInfo, // BR方式查询信息 .StartLocalListening = BrStartLocalListening, // BR方式开始监听 .StopLocalListening = BrStopLocalListening, // BR方式停止监听 .CheckActiveConnection = BrCheckActiveConnection, // BR方式检查活跃连接 .UpdateConnection = NULL, // BR不支持更新 .PreventConnection = BrPendConnection, // BR支持阻止连接 .ConfigPostLimit = ConnBrTransConfigPostLimit, // BR流量控制 };BLE策略(蓝牙低功耗连接)
在 softbus_conn_ble_manager.c:2360 中填充:
static ConnectFuncInterface bleFuncInterface = { .ConnectDevice = BleConnectDevice, .PostBytes = ConnBlePostBytes, .DisconnectDevice = BleDisconnectDevice, .DisconnectDeviceNow = BleDisconnectDeviceNow, .GetConnectionInfo = BleGetConnectionInfo, .StartLocalListening = BleStartLocalListening, .StopLocalListening = BleStopLocalListening, .CheckActiveConnection = BleCheckActiveConnection, .UpdateConnection = BleUpdateConnection, // BLE支持更新(如调整优先级) .PreventConnection = NULL, // BLE不支持阻止连接 .ConfigPostLimit = ConnBleTransConfigPostLimit, };TCP策略(WiFi局域网连接)
在 softbus_tcp_connect_manager.c:767 中填充:
static void InitTcpInterface(void) { g_tcpInterface.ConnectDevice = TcpConnectDevice; g_tcpInterface.DisconnectDevice = TcpDisconnectDevice; g_tcpInterface.DisconnectDeviceNow = TcpDisconnectDeviceNow; g_tcpInterface.PostBytes = TcpPostBytes; g_tcpInterface.GetConnectionInfo = TcpGetConnectionInfo; g_tcpInterface.StartLocalListening = TcpStartListening; g_tcpInterface.StopLocalListening = TcpStopListening; g_tcpInterface.CheckActiveConnection = TcpCheckActiveConnection; g_tcpInterface.UpdateConnection = NULL; // TCP不支持更新 g_tcpInterface.PreventConnection = NULL; // TCP不支持阻止连接 }关键观察:三种策略对同一接口方法的实现完全不同,而且有些方法设为NULL(表示该连接类型不支持此操作)。这正是策略模式的精髓——统一接口,差异实现。
2.4 上下文:连接管理器的策略注册与路由
2.4.1 策略注册(初始化时绑定具体策略)
ConnectFuncInterface *g_connManager[CONNECT_TYPE_MAX] = { 0 }; // 策略数组 int32_t ConnServerInit(void) { // ... 前置初始化 ... // 将三种具体策略注册到策略数组中 if (isSupportTcp) { g_connManager[CONNECT_TCP] = ConnInitTcp(&g_connManagerCb); // TCP策略 } g_connManager[CONNECT_BR] = ConnInitBr(&g_connManagerCb); // BR策略 g_connManager[CONNECT_BLE] = ConnInitBle(&g_connManagerCb); // BLE策略 }这就是策略模式的注册阶段——把"川菜师傅""粤菜师傅""鲁菜师傅"安排到后厨的对应工位上。
2.4.2 策略路由(运行时分发到正确策略)
连接管理器通过connectionId的高16位来识别连接类型,从而路由到正确的策略:
#define CONNECT_TYPE_SHIFT 16 // connectionId = (ConnectType << 16) | 序号 // 例如:BR连接ID = (2 << 16) | 5 = 0x00020005路由分发示例——连接设备:
int32_t ConnConnectDevice(const ConnectOption *info, uint32_t requestId, const ConnectResult *result) { // 1. 类型校验 if (ConnTypeCheck(info->type) != SOFTBUS_OK) { return ERROR; } // 2. 策略路由:根据类型直接索引到对应策略,调用其ConnectDevice方法 return g_connManager[info->type]->ConnectDevice(info, requestId, result); }路由分发示例——发送数据:
int32_t ConnPostBytes(uint32_t connectionId, ConnPostData *data) { // 1. 从connectionId提取连接类型 ConnectType type = (connectionId >> CONNECT_TYPE_SHIFT); // 2. 策略路由:直接索引到对应策略 return g_connManager[type]->PostBytes(connectionId,>int32_t ConnDisconnectDevice(uint32_t connectionId) { ConnectType type = (connectionId >> CONNECT_TYPE_SHIFT); return g_connManager[type]->DisconnectDevice(connectionId); }2.5 完整的策略模式流转图
2.6 与经典策略模式的差异
连接模块的实现与教科书上的策略模式有一些务实的差异,这些差异体现了嵌入式系统的工程智慧:
| 差异点 | 经典策略模式 | 连接模块实现 | 原因 |
|---|---|---|---|
| 策略数量 | 通常2-3个,运行时切换 | 3个固定策略,初始化时注册 | 连接类型在系统生命周期内不变 |
| 策略选择 | 由Context决定用哪个 | 由调用方(ConnectType)或连接ID决定 | 调用方明确知道要用哪种连接 |
| 策略存储 | 单个策略指针 | 策略数组g_connManager[MAX] | 需要同时管理多种策略 |
| 接口实现 | 所有方法必须实现 | 允许NULL(不支持的操作) | 不是每种连接都支持所有操作 |
| 语言机制 | C++虚函数/接口继承 | C语言函数指针结构体 | 系统级服务要求C语言、高性能 |
三、为什么要使用策略模式?
3.1 如果不用策略模式会怎样?
让我们想象一下没有策略模式的代码会是什么样:
// ❌ 反面教材:不用策略模式的实现 int32_t ConnConnectDevice(const ConnectOption *info, ...) { if (info->type == CONNECT_TCP) { // TCP连接的100行逻辑... socket = socket(AF_INET, SOCK_STREAM, 0); connect(socket, ...); // ... } else if (info->type == CONNECT_BR) { // BR连接的200行逻辑... SppConnect(info->brOption.brMac); // 状态机处理... // ACL碰撞处理... // ... } else if (info->type == CONNECT_BLE) { // BLE连接的300行逻辑... GattConnect(info->bleOption.bleMac); // 服务发现... // MTU协商... // 特征订阅... // ... } } int32_t ConnPostBytes(uint32_t connectionId, ...) { if (type == CONNECT_TCP) { /* TCP发送逻辑 */ } else if (type == CONNECT_BR) { /* BR发送逻辑 */ } else if (type == CONNECT_BLE) { /* BLE发送逻辑 */ } } // 每个函数都要重复同样的 if-else 分支!问题一目了然:
| 问题 | 严重程度 | 说明 |
|---|---|---|
| 代码膨胀 | 🔴 严重 | 每个函数都塞满所有连接类型的逻辑,一个文件可能上万行 |
| 修改扩散 | 🔴 严重 | 修改BR连接逻辑,要在一个巨大函数中找到对应分支,容易改错 |
| 新增困难 | 🔴 严重 | 新增WiFi Direct连接,要修改每一个函数的if-else |
| 无法独立测试 | 🟡 中等 | 无法单独测试BR逻辑,必须带着TCP和BLE一起编译 |
| 编译依赖 | 🟡 中等 | 所有连接类型的头文件互相纠缠 |
3.2 使用策略模式后的收益
收益1:消除条件分支(最直观的收益)
之前:每个函数都有if-else分支判断类型之后:一行代码直接路由
// ✅ 策略模式:一行搞定,无需if-else return g_connManager[info->type]->ConnectDevice(info, requestId, result);这就是策略模式最核心的威力——用多态替代条件分支。
收益2:关注点分离(每个策略独立开发)
BR开发工程师 ──→ 只看 br/ 目录,不需要理解BLE和TCP BLE开发工程师 ──→ 只看 ble/ 目录,不需要理解BR和TCP TCP开发工程师 ──→ 只看 tcp/ 目录,不需要理解BR和BLE每个连接类型是完全独立的编译单元,有自己的:
- 状态机(
ConnBrState/ConnBleState) - 连接实例(
ConnBrConnection/ConnBleConnection) - 数据传输(
ConnBrTrans/ConnBleTrans) - 发送队列(
ConnBrSendQueue/ConnBleSendQueue)
收益3:开放封闭原则(OCP)—— 新增连接类型零修改
假设未来要新增一种NFC连接,只需要:
// 第1步:实现NFC策略(新增文件,不修改任何现有文件) static ConnectFuncInterface nfcFuncInterface = { .ConnectDevice = NfcConnectDevice, .PostBytes = NfcPostBytes, .DisconnectDevice = NfcDisconnectDevice, // ... }; // 第2步:在初始化中注册(仅增加一行) g_connManager[CONNECT_NFC] = ConnInitNfc(&g_connManagerCb);不需要修改:
- ❌ 不改
ConnectFuncInterface接口定义 - ❌ 不改
ConnConnectDevice()路由逻辑 - ❌ 不改 BR/BLE/TCP 的任何代码
- ❌ 不改上层业务调用代码
这就是对扩展开放,对修改封闭。
收益4:接口契约的约束力
ConnectFuncInterface不仅是策略接口,更是契约——它强制要求每种连接类型必须实现核心方法(ConnectDevice、PostBytes、DisconnectDevice等),否则注册时会设为NULL,调用时会被拦截:
if (g_connManager[type]->PostBytes == NULL) { return SOFTBUS_CONN_MANAGER_OP_NOT_SUPPORT; // 明确告知不支持 }这比if-else方式更安全——不支持的操作有明确的错误码返回,而不是遗漏分支导致未定义行为。
收益5:运行时能力探测
策略模式天然支持能力探测——通过检查策略接口的函数指针是否为NULL,可以知道某种连接类型支持哪些操作:
| 操作 | BR | BLE | TCP |
|---|---|---|---|
ConnectDevice | ✅ | ✅ | ✅ |
PostBytes | ✅ | ✅ | ✅ |
DisconnectDevice | ✅ | ✅ | ✅ |
UpdateConnection | ❌ | ✅ | ❌ |
PreventConnection | ✅ | ❌ | ❌ |
ConfigPostLimit | ✅ | ✅ | ❌ |
这种差异化能力在if-else方式中很难优雅地表达。
3.3 为什么用C函数指针而非C++虚函数?
这是很多人会问的问题。连接模块选择C语言函数指针实现策略模式,有深层的技术考量:
| 对比维度 | C++虚函数 | C函数指针 |
|---|---|---|
| 运行时开销 | vtable间接寻址(2次指针解引用) | 直接函数指针调用(1次解引用) |
| 内存布局 | 每个对象额外4/8字节vptr | 无额外开销 |
| ABI稳定性 | 受编译器影响(vtable布局可能不同) | 函数指针布局稳定 |
| 兼容性 | 需要C++运行时支持 | 纯C,无运行时依赖 |
| 内核态可用 | ❌ 内核通常不支持C++ | ✅ 内核广泛使用函数指针 |
| 调试可读性 | 虚函数调用栈不够直观 | 函数指针名称可打印 |
结论:对于分布式软总线这种系统级基础服务,C函数指针是比C++虚函数更合适的选择——更轻量、更稳定、更通用。
3.4 策略模式与观察者模式的协同
连接模块中,策略模式和观察者模式不是孤立存在的,而是协同工作形成完整的架构:
具体协同流程(以BR连接建立为例):
1. 上层调用 ConnConnectDevice(BR类型) → 策略模式路由到 g_connManager[BR]->ConnectDevice() → BrConnectDevice() 执行BR连接逻辑 2. BR连接成功后,调用 g_connManagerCb.OnConnected() → ConnManagerConnected() 触发观察者模式 → 遍历 g_listenerList,通知所有业务模块 → 认证模块.OnConnected() → 启动认证 → 通道模块.OnConnected() → 创建通道 → 代理模块.OnConnected() → 调整策略策略模式负责"往下走"(选择实现),观察者模式负责"往上走"(通知结果),两者一上一下,构成了完整的控制流和数据流。
总结:策略模式的设计精髓
核心价值一句话
策略模式让连接模块实现了"一套接口、多种实现、按需路由"——上层只管"我要连接",不用管"怎么连接";底层各自实现"怎么连接",不用管"谁在调用"。
设计原则落地总结
| 设计原则 | 策略模式如何落地 |
|---|---|
| 开闭原则(OCP) | 新增连接类型只需新增策略实现+注册一行,零修改现有代码 |
| 单一职责(SRP) | 每种连接策略只负责自己的连接逻辑,管理器只负责路由 |
| 里氏替换(LSP) | 所有策略都可替换,上层代码不依赖具体策略 |
| 接口隔离(ISP) | ConnectFuncInterface只包含连接相关方法,不掺杂业务逻辑 |
| 依赖倒置(DIP) | 管理器依赖抽象接口ConnectFuncInterface,不依赖具体BR/BLE/TCP实现 |
与现实生活的类比
把连接模块想象成一个快递公司:
| 角色 | 快递公司 | 连接模块 |
|---|---|---|
| 策略接口 | "配送"这个动作 | ConnectFuncInterface |
| 具体策略 | 陆运/空运/海运三种配送方式 | TCP/BR/BLE三种连接方式 |
| 上下文 | 调度中心根据包裹类型选择配送方式 | ConnManager根据ConnectType路由 |
| 观察者 | 客户收到配送通知 | 业务模块收到连接事件回调 |
调度中心(上下文)不需要知道陆运怎么开车、空运怎么开飞机,只需要知道"配送"这个动作——这就是策略模式的本质:把"做什么"和"怎么做"彻底分开。