概述
在没有面向对象语法的C语言中,策略(Strategy)模式和状态(State)模式都通过“上下文 + 接口”组合来模拟多态。
它们在代码结构上几乎一致,但设计意图和应用场景却差异很大。
本文分三部分深入剖析:
- 在C语言中如何模拟多态
- 策略模式与状态模式的设计意图与差异
- 结合EEPROM抽象层案例,展示二者协作与状态机应用
1. C语言中的“模拟多态”
在C语言中,多态可通过结构体与函数指针模拟:上下文(Context)持有一组函数指针(vtable),动态指向具体实现。下例展示通用框架:
// 通用接口类型(vtable)
typedef struct {void (*action)(void *ctx, int evt);
} module_iface_t;// 通用上下文
typedef struct {const module_iface_t *iface; // 指向当前策略或状态的 vtablevoid *instance_data; // 指向具体策略/状态实例私有数据
} module_ctx_t;
在策略模式中,action
对应具体算法;在状态模式中,action
则为事件处理函数。此框架既可用于策略,也可用于状态,赋予系统极高的复用性。
2. 策略模式 vs. 状态模式
2.1 策略模式:关注“如何做”
- 目的:封装一组可互换的算法,让调用者在运行时自行选择
- 决策权:在外部,上下文被动执行指定策略
- 应用场景:滤波算法切换、通信协议选择、后端存储后备机制等
示例类图
2.2 状态模式:关注“何时做、做什么”
- 目的:将对象行为与内部状态绑定,使行为随状态自动切换
- 决策权:在内部,状态自身决定是否及何时转换
- 应用场景:设备驱动、协议状态机、时序复杂的后端服务等
示例类图与状态图
核心差异
- 策略模式:算法平行,可在任意时刻替换
- 状态模式:状态有序,转换逻辑由状态机管理
2.3 小结:模式赋能架构
在前文中,我们分别阐述了策略模式如何将“如何做”的决策权交给外部,做到算法与调用逻辑的彻底解耦;以及状态模式如何把“何时做、做什么”的流程控制封装到内部状态机中,保障行为随状态演进而自动切换。
既然二者在实现上高度相似,我们有时候可以将二者组合起来使用。
3. 实战:高可靠EEPROM抽象层
下面通过分层设计,展示策略模式与状态机的协同应用。
3.1 统一层:策略模式应用
需求
为应用暴露统一接口,支持I2C EEPROM与NOR Flash两种后端,具备自动检测与运行时降级能力。
设计
- 上下文:
unified_eeprom
- 策略接口:包含
read_var
,write_var
等函数指针 - 具体策略:
I2C_EEPROM_Service
、EE_NOR_Flash_Service
- 决策枚举:
eeprom_backend_t { AUTO, I2C, FLASH, HYBRID }
// 策略接口定义
typedef struct {rt_err_t (*read_var)(int id, void *buf, size_t len);rt_err_t (*write_var)(int id, const void *buf, size_t len);
} eeprom_iface_t;// 统一上下文
typedef struct {const eeprom_iface_t *iface;eeprom_backend_t current_backend;
} unified_eeprom_t;
运行时降级
eeprom_set_backend(EEPROM_BACKEND_AUTO);rt_err_t rc = eeprom_read_var(VAR_ID_X, buf, len);
if (rc != RT_EOK) {// 失败后自动切换策略eeprom_set_backend(EEPROM_BACKEND_FLASH);rc = eeprom_read_var(VAR_ID_X, buf, len);
}
AUTO模式下,统一层在I2C失败时自动切换至Flash。此决策过程对上层透明,上层仅感知最终结果。
3.2 后端服务:HSM驱动的状态模式
将I2C和Flash服务均设计为分层状态机,封装所有时序逻辑,实现可恢复与高可靠。
3.2.1 I2C EEPROM状态机
- 关键状态:
UNINITIALIZED
、IDLE
、READING
、WRITING
、WAITING_READY
、VERIFYING
、ERROR
- 核心逻辑:跨页读取、分页写入+轮询就绪、可选校验与指数退避重试
3.2.2 NOR Flash状态机
- 关键状态:
UNINITIALIZED
、VALID_PAGE
、MIGRATING
、FORMATTING
、ERROR
- 核心逻辑:双页交替写、数据迁移、启动恢复与磨损均衡
4. 协同:策略 × 状态机
在“高可靠EEPROM抽象层”案例中,我们已经看到了策略模式与状态机的协同:
- 策略层 (统一层):决定“走哪条路径”,即选择I2C后端还是Flash后端。
- 状态机层 (后端服务):负责“路径内部的时序与恢复”,如I2C的写后轮询、Flash的页面迁移。
两者协同时,可在复杂约束下实现透明降级与高可用。
这种“策略包含状态机”的组合非常强大,下面我们深入探讨其两种主要协同模式。
4.1 模式一:将“完整状态机”作为策略单元
这是EEPROM案例的核心架构模式。我们把一个“完整的状态机(包含其所有状态、转移逻辑和动作)”封装成一个策略实现。Context仅需持有指向不同状态机策略的指针,即可在运行时“一键切换整体行为”。
这相当于将I2C_EEPROM_Service
和EE_NOR_Flash_Service
这两个复杂的状态机,包装成符合eeprom_iface_t
接口的策略单元。
适用场景:
- 复杂流程的替换:将“I2C读写流程”与“Flash读写流程”抽象为可互换的策略。
- 行为隔离:两套状态机逻辑完全独立,互不干扰,便于独立开发与测试。
4.2 模式二:在“状态内部”嵌入子策略
当状态机的主流程固定,但某个具体状态的“动作”或“算法”需要灵活替换时,可以在该状态内部嵌入一个“子策略”。
例如,在Flash_EEPROM_Service
的MIGRATING
状态中,数据校验可以是一个子策略。我们可以根据系统要求,在“快速校验(CRC8)”和“高可靠校验(CRC32)”之间切换,而无需改变状态机的主体结构。
// MIGRATING状态的动作函数
void on_migrating_entry(hsm_t *sm, const hsm_event_t *event) {// ...// 根据运行时配置选择校验策略if (is_fast_mode()) {ctx->crc_strategy = &crc8_strategy;} else {ctx->crc_strategy = &crc32_strategy;}// ...
}// 在迁移过程中使用子策略
void perform_migration_step(context_t *ctx) {// ...uint32_t crc = ctx->crc_strategy->calculate(data, len);// ...
}
适用场景:
- 算法替换:状态机结构稳定,但内部算法需要动态调整(如加密、压缩、校验算法)。
- 功能开关:在不改变状态流程的情况下,通过空策略(noop)实现功能的动态开启或关闭。
4.3 C代码示例:用策略管理多状态机
下面的代码演示了“模式一”的通用实现,即如何通过策略模式来管理和切换两个完全独立的状态机流程。
// "状态机策略"的统一接口
typedef struct sm_ctx_s sm_ctx_t;
typedef struct {void (*init)(sm_ctx_t*);void (*dispatch)(sm_ctx_t*, int event);
} sm_strategy_t;// 上下文,持有当前的状态机策略
struct sm_ctx_s {const sm_strategy_t *ops;void *state_data; // 指向具体状态机的私有数据
};// --- 状态机A (例如: I2C EEPROM) ---
void sma_init(sm_ctx_t *c){ /* ... 状态机A初始化 ... */ }
void sma_dispatch(sm_ctx_t *c, int e){ /* ... A的事件处理 ... */ }
const sm_strategy_t smA_strategy = { sma_init, sma_dispatch };// --- 状态机B (例如: Flash EEPROM) ---
void smb_init(sm_ctx_t *c){ /* ... 状态机B初始化 ... */ }
void smb_dispatch(sm_ctx_t *c, int e){ /* ... B的事件处理 ... */ }
const sm_strategy_t smB_strategy = { smb_init, smb_dispatch };// 应用层通过策略接口与状态机交互
void application_logic(void) {sm_ctx_t fsm;// 根据条件选择一个完整的状态机流程作为当前策略if (use_i2c_backend()) {fsm.ops = &smA_strategy;} else {fsm.ops = &smB_strategy;}// 后续操作对具体状态机无感知fsm.ops->init(&fsm);while (has_event()) {fsm.ops->dispatch(&fsm, next_event());}
}
实践要点(结合统一EEPROM案例):
- 策略层:
unified_eeprom
负责选择 I2C 或 Flash 后端(含 AUTO/降级),对上层透明。 - 状态机层:
i2c_eeprom_service
和ee_nor_flash_service
各自封装页写、迁移、恢复等复杂时序与错误处理。 - 并发与锁序:策略层持短锁做选择,状态机在各自内部串行化操作,避免跨层嵌套锁。
- 失败处理:策略层负责统计健康度并决策是否切换;状态机负责局部的、可恢复的重试。
5. 小结
- 在嵌入式C中,可通过“上下文 + 接口”框架复用策略与状态模式实现结构。
- 策略模式关注“如何做”,决策权在外部;状态模式关注“何时做、做什么”,决策权在内部。
- 将策略与状态机结合,可构建透明、可降级、高可用的分层架构。