设计模式(下):策略模式、命令模式、适配器模式的C语言实现

聊完了观察者和单例,咱们继续往下挖。今天要讲的这三个模式——策略、命令、适配器——在嵌入式里特别实用。我个人习惯把它们归为「行为与接口的变形金刚」。说白了,它们解决的是「怎么做」、「谁来做」和「怎么接」的问题。

策略模式:把算法拆出来,随时换

先说说策略模式。你想想看,一个系统里可能有多种算法,比如不同的加密方式、不同的排序方法。如果全写在一个大函数里,用一堆if-else去选,那代码会变成什么样?

嗯,我见过一个项目,里面光数据校验就写了四种算法,全塞在一个函数里。后来要加第五种,改代码的人差点崩溃。这就是典型的「坏味道」。

策略模式的做法很简单:把每种算法封装成一个独立的模块,然后通过一个公共接口去调用。这样加新算法就像换电池一样,插上就能用。

看个C语言的例子:

// 策略接口
typedef struct {
    void (*encrypt)(const char* input, char* output);
} EncryptStrategy;

// 策略A:简单异或
void xor_encrypt(const char* input, char* output) {
    while (*input) {
        *output++ = *input++ ^ 0x55;
    }
    *output = '\0';
}

// 策略B:取反
void not_encrypt(const char* input, char* output) {
    while (*input) {
        *output++ = ~(*input++);
    }
    *output = '\0';
}

// 上下文,持有策略
typedef struct {
    EncryptStrategy* strategy;
} Context;

void context_set_strategy(Context* ctx, EncryptStrategy* s) {
    ctx->strategy = s;
}

void context_encrypt(Context* ctx, const char* input, char* output) {
    ctx->strategy->encrypt(input, output);
}

你看,调用方只需要关心用哪个策略,不用管具体怎么加密。我在项目中遇到过类似场景:一个通信模块需要支持AES和SM4两种加密,用策略模式后,切换加密算法只需要改一行初始化代码。

核心思想:把「做什么」和「怎么做」分开。策略模式让算法可以独立变化,不影响调用方。

命令模式:把请求打包,慢慢处理

命令模式,说白了就是把一个操作封装成一个对象。为什么要这么做?因为有时候你不想立刻执行,或者你想把操作存起来、排队、甚至撤销。

我记得在做一个智能家居项目时,用户通过手机APP发送指令控制灯光、窗帘、空调。如果每个指令都直接操作硬件,那代码会耦合得很紧。而且用户可能同时发多个指令,需要排队执行。

命令模式正好解决这个问题。每个命令就是一个结构体,里面存着要执行的操作和参数。然后有个调度器统一管理这些命令。

// 命令接口
typedef struct {
    void (*execute)(void* args);
    void (*undo)(void* args);
} Command;

// 具体命令:开灯
typedef struct {
    Command base;
    int light_id;
} LightOnCommand;

void light_on_execute(void* args) {
    LightOnCommand* cmd = (LightOnCommand*)args;
    printf("打开灯 %d\n", cmd->light_id);
}

void light_on_undo(void* args) {
    LightOnCommand* cmd = (LightOnCommand*)args;
    printf("关闭灯 %d\n", cmd->light_id);
}

// 命令队列
typedef struct {
    Command* queue[64];
    int head, tail;
} CommandQueue;

void queue_push(CommandQueue* q, Command* cmd) {
    q->queue[q->tail++] = cmd;
}

void queue_process(CommandQueue* q) {
    while (q->head < q->tail) {
        Command* cmd = q->queue[q->head++];
        cmd->execute(cmd);
    }
}

这样做的好处很明显:命令可以排队、可以撤销、可以记录日志。我曾经在一个项目中用命令模式实现了「一键场景」功能——用户按一个按钮,系统自动执行一串命令,比如关灯、拉窗帘、调空调温度。如果没有命令模式,这种组合操作会写得很痛苦。

小技巧:命令模式特别适合做「操作历史」和「撤销/重做」。你只需要把每个命令的undo函数实现好,然后维护一个历史栈就行了。

适配器模式:让不兼容的接口一起工作

适配器模式,名字就说明了一切。它就像电源转换插头,让一个接口能适配另一个接口。

在嵌入式开发中,这种情况太常见了。比如你有一个旧的传感器驱动,它返回的是模拟电压值。但新的系统需要数字化的温度值。你不能改旧驱动(可能没源码),那就写个适配器。

适配器有两种实现方式:对象适配器和类适配器。C语言里我们通常用对象适配器,因为C没有继承,但可以用结构体组合。

// 旧接口:返回电压
typedef struct {
    float (*get_voltage)(void);
} OldSensor;

// 新接口:返回温度
typedef struct {
    float (*get_temperature)(void);
} NewSensor;

// 适配器
typedef struct {
    NewSensor base;       // 对外表现为新接口
    OldSensor* old;       // 内部持有旧接口
    float voltage_to_temp; // 转换系数
} SensorAdapter;

float adapter_get_temperature(void* self) {
    SensorAdapter* adp = (SensorAdapter*)self;
    float voltage = adp->old->get_voltage();
    return voltage * adp->voltage_to_temp;  // 转换
}

// 使用
OldSensor old_dev = { .get_voltage = read_adc };
SensorAdapter adp = {
    .base.get_temperature = adapter_get_temperature,
    .old = &old_dev,
    .voltage_to_temp = 100.0f
};
float temp = adp.base.get_temperature(&adp);

你看,适配器把旧接口「包装」成了新接口。调用方完全不知道底层是旧传感器,它只看到get_temperature这个函数。

注意:适配器模式不要滥用。如果新旧接口差异太大,适配器会变得很复杂。我曾经见过一个适配器,里面做了大量数据转换和状态同步,最后比直接重写驱动还麻烦。适配器适合「小修小补」,不适合「大动干戈」。

三种模式对比

模式 解决的问题 核心机制 典型场景
策略模式 算法选择与切换 接口+多实现 加密、排序、校验
命令模式 请求封装与延迟执行 命令对象+队列 指令队列、撤销操作
适配器模式 接口不兼容 包装+转换 旧驱动适配、第三方库集成

这三种模式在C语言里实现起来都不复杂,关键是要理解它们的设计意图。策略模式让你灵活换算法,命令模式让你优雅处理请求,适配器模式让你兼容旧代码。嗯,说白了,都是为了让代码更灵活、更可维护。

我个人建议,在写代码之前先想想:这个模块未来会不会变?如果会,那就考虑用模式把它「解耦」出来。别等到代码写了一大堆再重构,那时候就晚了。

三种设计模式核心逻辑对比 策略模式 算法选择与切换 Context(上下文) Strategy(接口) ConcreteA ConcreteB 算法独立变化 调用方不感知 命令模式 请求封装与延迟执行 Invoker(调用者) Command(接口) ConcreteA ConcreteB 支持队列/撤销 请求与执行解耦 适配器模式 接口不兼容时的桥梁 Client(调用方) Adapter(适配器) Adaptee(旧接口) 包装旧接口 对外统一新接口

最后说一句:设计模式不是银弹。别为了用模式而用模式。我见过有人在一个只有三个函数的模块里硬套策略模式,结果代码量翻了三倍,可读性反而下降了。模式是工具,不是目的。用对地方,事半功倍;用错地方,事倍功半。

专注资料整理