策略模式:函数指针表、策略切换、运行时多态
策略模式,说白了就是「把算法抽出来,让它们可以互相替换」。我刚开始学设计模式时,总觉得这东西是 Java 的专利。后来在嵌入式项目里写 C 语言,发现函数指针就是天然的「策略接口」。嗯,今天我们就聊聊怎么用 C 实现策略模式。
为什么需要策略模式?
你想想看,一个系统里经常会有多种算法。比如数据压缩,你可以用 zlib、lz4、或者自己写的简单算法。如果把这些算法硬编码到业务逻辑里,代码会变成一团乱麻。
我在项目中遇到过这样一个场景:一个通信模块需要支持多种加密算法。最开始用 switch-case 来选算法,后来加了三种新算法,代码直接膨胀到 2000 行。每次改一个算法,都要重新编译整个模块。这谁受得了?
策略模式的核心思想很简单:定义一组算法,把它们封装起来,让它们可以互相替换。在 C 语言里,函数指针表就是最直接的实现方式。
函数指针表:策略的容器
我们先定义一个策略的「接口」。在 C 里没有 interface 关键字,但可以用结构体来模拟。
// 策略接口:一个操作函数指针
typedef int (*compress_func_t)(const char* input, size_t in_len,
char* output, size_t* out_len);
// 策略上下文:持有当前策略
typedef struct {
compress_func_t compress;
const char* name;
} compress_strategy_t;
然后我们实现几个具体的策略:
// 策略A:快速压缩
int compress_fast(const char* input, size_t in_len,
char* output, size_t* out_len) {
// 简单的 RLE 压缩
// ... 实现细节
return 0;
}
// 策略B:高压缩比
int compress_high(const char* input, size_t in_len,
char* output, size_t* out_len) {
// 使用 zlib 库
// ... 实现细节
return 0;
}
// 策略C:不压缩(透传)
int compress_none(const char* input, size_t in_len,
char* output, size_t* out_len) {
memcpy(output, input, in_len);
*out_len = in_len;
return 0;
}
现在,我们把策略注册到一个表里:
// 策略注册表
static compress_strategy_t strategy_table[] = {
{compress_fast, "fast"},
{compress_high, "high"},
{compress_none, "none"},
};
#define STRATEGY_COUNT (sizeof(strategy_table) / sizeof(strategy_table[0]))
这样做的好处是什么?新增一个策略,只需要在表里加一行。业务代码完全不用改。我在项目中用这个模式管理了 12 种加密算法,每次加新算法只需要 5 分钟。
策略切换:运行时动态选择
策略切换有两种方式:编译时切换和运行时切换。我们重点说运行时切换。
运行时切换的核心是「根据条件选择策略」。比如根据配置文件、用户输入、或者系统状态来切换。
// 根据名称查找策略
compress_strategy_t* find_strategy(const char* name) {
for (int i = 0; i < STRATEGY_COUNT; i++) {
if (strcmp(strategy_table[i].name, name) == 0) {
return &strategy_table[i];
}
}
return NULL; // 没找到,返回 NULL
}
// 使用策略
void process_data(const char* data, size_t len, const char* algo) {
compress_strategy_t* strategy = find_strategy(algo);
if (!strategy) {
printf("不支持的算法: %s\n", algo);
return;
}
char buffer[1024];
size_t out_len = 0;
strategy->compress(data, len, buffer, &out_len);
printf("使用 %s 压缩,压缩率: %.2f%%\n",
strategy->name, (float)out_len / len * 100);
}
我曾经踩过一个坑:在中断上下文里切换策略。中断处理函数里调用了策略函数,结果策略函数里用了 malloc。嗯,中断里用 malloc 会死锁。后来我改成在初始化时预分配所有策略的内存,中断里只做指针切换。
运行时多态:C 语言的「虚函数」
面向对象语言里的多态,在 C 里就是函数指针。每个策略实例可以有自己的「虚函数表」。我们来看一个更复杂的例子:
// 策略对象:包含数据和操作
typedef struct {
void* data; // 策略私有数据
int (*init)(void*); // 初始化
int (*process)(void*, const char*, size_t, char*, size_t*);
void (*cleanup)(void*); // 清理
} strategy_object_t;
// 创建具体策略
strategy_object_t* create_fast_strategy(void) {
strategy_object_t* obj = malloc(sizeof(strategy_object_t));
obj->data = malloc(sizeof(fast_private_t));
obj->init = fast_init;
obj->process = fast_process;
obj->cleanup = fast_cleanup;
return obj;
}
这样每个策略可以有自己的私有数据,实现了真正的「封装」。调用方不需要知道策略内部细节,只需要调用统一的接口。
策略模式的应用场景
我个人习惯在以下几种场景使用策略模式:
- 算法族管理:加密、压缩、排序、校验等
- 协议解析:不同版本的协议有不同的解析逻辑
- 设备驱动:同一类设备的不同型号,操作方式不同
- 状态机:每个状态的处理逻辑可以看作一个策略
举个例子,我在做物联网网关时,需要支持多种传感器协议。每个传感器厂商的协议都不一样。我用策略模式管理了 8 种协议,每种协议一个策略。新接入一个传感器,只需要写一个策略文件,注册到表里就行。业务代码一行都不用改。
策略模式的优缺点
| 优点 | 缺点 |
|---|---|
| 算法可以独立变化,互不影响 | 策略数量多时,管理成本增加 |
| 新增策略不需要改已有代码 | 调用方需要知道有哪些策略可用 |
| 符合开闭原则 | 函数指针调用有轻微性能开销 |
| 代码结构清晰,易于测试 | 策略之间可能有依赖关系 |
关于性能开销,我实测过:函数指针间接调用比直接调用多 2-3 个 CPU 周期。在大多数场景下可以忽略不计。但如果你的策略函数被每秒调用百万次,那就要考虑内联或者编译时策略选择了。
策略模式的核心逻辑图
下面这张图展示了策略模式的核心结构:
避坑指南
我总结几个常见的坑,都是我曾经踩过的:
- 策略函数不要有状态依赖:如果策略函数依赖全局变量,切换策略时可能会出问题。建议策略函数只依赖传入的参数和自身的私有数据。
- 注意线程安全:多个线程同时切换策略,可能导致策略指针不一致。可以用读写锁或者 RCU 来保护。
- 策略注册表要排序:如果策略很多,用二分查找比线性查找快得多。我一般按名称排序,查找时用 bsearch。
- 默认策略兜底:当找不到匹配策略时,不要直接返回错误。提供一个默认策略,比如「不做任何操作」,保证系统能继续运行。
REGISTER_STRATEGY(name, func) 自动把策略加入表里。这样新增策略时只需要写一行宏,连表都不用改。
总结
策略模式在 C 语言里就是函数指针的灵活运用。它让代码变得可扩展、可维护。说白了,就是把「做什么」和「怎么做」分开。你只需要关心调用哪个策略,不用关心策略内部怎么实现。
我在项目中用这个模式重构过很多次代码。每次重构完,代码量减少了,可读性提高了,测试也更容易了。如果你现在还在用 switch-case 管理算法,不妨试试策略模式。相信我,你会爱上这种感觉。
公众号:蓝海资料掘金营,微信deep3321