策略模式:函数指针表、策略切换、运行时多态

策略模式,说白了就是「把算法抽出来,让它们可以互相替换」。我刚开始学设计模式时,总觉得这东西是 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 会死锁。后来我改成在初始化时预分配所有策略的内存,中断里只做指针切换。

注意: 在中断、临界区、或者实时性要求高的场景下,策略函数不能有阻塞操作(malloc、printf、锁等)。建议在初始化阶段完成所有资源分配,运行时只做纯计算。

运行时多态: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 周期。在大多数场景下可以忽略不计。但如果你的策略函数被每秒调用百万次,那就要考虑内联或者编译时策略选择了。

策略模式的核心逻辑图

下面这张图展示了策略模式的核心结构:

策略模式核心结构 上下文 (Context) 持有策略指针 调用策略接口 运行时切换策略 策略接口 函数指针表 统一操作接口 策略注册表 具体策略A 算法实现A 私有数据A 如:快速压缩 具体策略B 算法实现B 私有数据B 如:高压缩比 具体策略C 算法实现C 私有数据C 如:不压缩 持有 注册/选择 运行时切换 策略模式:将算法封装为独立策略,通过函数指针表实现运行时多态

避坑指南

我总结几个常见的坑,都是我曾经踩过的:

  • 策略函数不要有状态依赖:如果策略函数依赖全局变量,切换策略时可能会出问题。建议策略函数只依赖传入的参数和自身的私有数据。
  • 注意线程安全:多个线程同时切换策略,可能导致策略指针不一致。可以用读写锁或者 RCU 来保护。
  • 策略注册表要排序:如果策略很多,用二分查找比线性查找快得多。我一般按名称排序,查找时用 bsearch。
  • 默认策略兜底:当找不到匹配策略时,不要直接返回错误。提供一个默认策略,比如「不做任何操作」,保证系统能继续运行。
小技巧: 可以用宏来简化策略注册。比如 REGISTER_STRATEGY(name, func) 自动把策略加入表里。这样新增策略时只需要写一行宏,连表都不用改。

总结

策略模式在 C 语言里就是函数指针的灵活运用。它让代码变得可扩展、可维护。说白了,就是把「做什么」和「怎么做」分开。你只需要关心调用哪个策略,不用关心策略内部怎么实现。

我在项目中用这个模式重构过很多次代码。每次重构完,代码量减少了,可读性提高了,测试也更容易了。如果你现在还在用 switch-case 管理算法,不妨试试策略模式。相信我,你会爱上这种感觉。


公众号:蓝海资料掘金营,微信deep3321