1. 概述
在没有面向对象语法的C语言中,策略(Strategy)模式和状态(State)模式都通过"上下文 + 接口"组合来模拟多态。
它们在代码结构上几乎一致,但设计意图和应用场景却差异很大。
本文将结合典型的滤波算法和USB设备示例,深入解析两者的实现差异与选型建议。
2. 两种模式介绍
- 策略模式:将一组算法封装起来,调用者在运行时决定使用哪一种,关注"如何做"。
策略模式类图
sensor_t +filter_strategy_t *strat +data[128] <<interface>> filter_strategy_t +filter(buf, len) mean_strat kalman_strat
- 状态模式:让对象在内部状态变化时自行切换行为,关注"何时做、做什么"。
状态模式类图
usb_dev_t +usb_state_t *state <<interface>> usb_state_t +handle(dev, evt) st_disconnected st_connecting st_ready st_error
状态转换图
EVT_PLUG_IN EVT_ENUM_OK EVT_ENUM_FAIL Disconnected Connecting Ready Error
3. 实现结构其实类似
c
// 通用接口类型(vtable)
typedef struct {
void (*action)(void *ctx, int evt);
} module_iface_t;
// 通用上下文
typedef struct {
const module_iface_t *iface; // 指向当前策略或状态
void *state_data; // 具体实例数据
} module_ctx_t;
上述结构在策略和状态模式下均可重用:仅需替换 action
策略函数或 handle_event
状态函数。
4. 策略模式详解
4.1 典型场景
一个传感器需要根据功耗或精准度要求,动态选用不同滤波算法。
4.2 代码示例
c
// 策略接口
typedef struct {
void (*filter)(int *buf, int len);
} filter_strategy_t;
// 上下文
typedef struct {
const filter_strategy_t *strat;
int data[128];
} sensor_t;
// 均值滤波
void mean_filter(int *buf, int len) { /* ... */ }
const filter_strategy_t mean_strat = { .filter = mean_filter };
// 卡尔曼滤波
void kalman_filter(int *buf, int len) { /* ... */ }
const filter_strategy_t kalman_strat = { .filter = kalman_filter };
// 应用层切换策略
void sensor_process(sensor_t *s) {
if (is_low_power()) {
s->strat = &mean_strat;
} else {
s->strat = &kalman_strat;
}
s->strat->filter(s->data, 128);
}
4.3 策略模式特点
- 切换由外部决定
- 上下文仅负责调用,不关心何时切换
- 适合算法族、可插拔的行为
5. 状态模式详解
5.1 典型场景
一个USB设备在不同生命周期阶段:断开、连接中、就绪、错误,各阶段有不同处理逻辑,并能自动流转。
5.2 代码示例
c
typedef struct usb_dev_s usb_dev_t;
// 状态接口
typedef struct {
void (*handle)(usb_dev_t *dev, int evt);
} usb_state_t;
// 上下文
struct usb_dev_s {
const usb_state_t *state;
// 设备相关数据
};
// 断开状态
void st_disconnected(usb_dev_t *d, int evt) {
if (evt == EVT_PLUG_IN) {
d->state = &st_connecting;
start_enumeration();
}
}
const usb_state_t st_disconnected = { .handle = st_disconnected };
// 连接中状态
void st_connecting(usb_dev_t *d, int evt) {
if (evt == EVT_ENUM_OK) {
d->state = &st_ready;
} else if (evt == EVT_ENUM_FAIL) {
d->state = &st_error;
}
}
const usb_state_t st_connecting = { .handle = st_connecting };
// 事件分发
void usb_event(usb_dev_t *d, int evt) {
d->state->handle(d, evt);
}
6. 策略模式与状态模式的状态机化融合
6.1 概述
实现了策略模式与状态模式的深度融合,既能在运行时无缝切换不同流程,也能在状态内部灵活替换子算法,为复杂嵌入式控制系统提供高内聚、低耦合、可配置的可复用框架。
在嵌入式系统(如固件升级、多协议网关、自适应控制)中,通常面临两类变化点:
- 横向变化:整条业务流程需要整体替换(例如升级流程与正常流程)。
- 纵向变化:流程内部某一状态的子算法需要动态替换(例如根据网络质量切换可靠传输与快速传输)。
如果采用传统的switch-case
实现,常会引发以下困境:
- 新增流程时需要阅读理解整份代码。
- 运行时切换流程通常需重启或清零全局变量,造成业务中断。
- 难以针对单一流程进行单元测试证。
6.2 策略-状态两级抽象
实现了策略模式与状态模式的深度融合,既能在运行时无缝切换不同流程,也能在状态内部灵活替换子算法,为复杂嵌入式控制系统提供高内聚、低耦合、可配置的可复用框架。
第一级:整状态机作为策略
将完整状态机(含状态表、变量、定时器与转移逻辑)封装为统一策略接口 sm_strategy_t
。
Context 在运行时只需持有对该接口的指针,即可灵活切换整个状态机实现。
c
sm_ctx_t fsm;
fsm.ops = use_upgrade ? &sm_upgrade : &sm_normal;
fsm.ops->init(&fsm);
第二级:状态内部再嵌策略
在某个具体状态的处理函数中,根据运行时条件动态注入子策略,以替换子算法或业务动作。
c
/* 在 Transferring 状态中,根据网络质量选择子策略 */
ctx->sub = is_lossy()
? &reliable_xfer_strategy
: &fast_xfer_strategy;
ctx->sub->chunk_size = calc_chunk_size(ctx->rtt);
ctx->sub->send(ctx->sub, evt);
两级策略各自独立:
- 第一级解决"流程级"整体切换。
- 第二级解决"状态级"内部微调。
6.3 静态结构
sm_ctx_t +const sm_strategy_t *ops +void *state_data <<interface>> sm_strategy_t +init(sm_ctx_t*) +dispatch(sm_ctx_t*, int) sm_normal sm_upgrade
6.4 运行时视图
选择策略 normal upgrade Application strategy sm_normal FSM sm_upgrade FSM state1 state2 stateA stateB
6.5 关键实现细节
策略接口定义
c
typedef struct sm_ctx_s sm_ctx_t;
typedef struct {
void (*init)(sm_ctx_t*);
void (*dispatch)(sm_ctx_t*, int);
} sm_strategy_t;
运行时上下文
c
struct sm_ctx_s {
const sm_strategy_t *ops; /* 当前流程策略 */
void *state_data;
};
零停机切换
通过原子写入指针即可完成业务流程切换,无需中断或重启。
c
void switch_strategy(sm_ctx_t *ctx, const sm_strategy_t *new_ops)
{
new_ops->init(ctx);
ctx->ops = new_ops; /* 原子赋值,零停机 */
}
状态内子策略示例
c
typedef struct {
void (*send)(const void *self, int event);
size_t chunk_size;
} xfer_strategy_t;
static void st_transferring(sm_ctx_t *ctx, int evt)
{
xfer_strategy_t *sub = select_xfer_strategy(ctx->rtt);
sub->chunk_size = calc_chunk(ctx->rtt);
sub->send(sub, evt);
}