23 种设计模式详解 - 11 种行为型模式

做嵌入式久了,会发现一个规律:驱动代码,最难的不是"怎么操作硬件",而是"怎么把一堆交互逻辑理顺"

比如写一个带 MQTT 上报的温控器,按键要响应、传感器要采样、断网要重连、告警要推送、Flash 要存参数......这些模块互相调用、互相通知、互相等待。如果全用全局变量 + if-else ,半年后这代码自己都不敢动。

GoF 的 行为型模式(11 种) 就是前人总结的"对象交互套路"。它不关心怎么造对象(那是创建型的活),也不关心怎么搭结构(结构型的活),它只解决一件事:对象之间怎么通信、职责怎么分、算法怎么换,才能不耦合、好扩展、好维护

C 语言没有类、没有继承,但"行为型模式"的核心思想------把变化的部分封装、把交互的路径理顺------在 C 里照样能用,而且用对了效果惊人。FreeRTOS、RT-Thread、lwIP 这些项目里到处都是。


行为型模式总览

模式 一句话 嵌入式典型场景
责任链 请求沿链传递,谁处理谁停 AT 指令解析、日志分级、按键事件
命令 把动作封装成对象 串口命令分发、按键队列、撤销重做
解释器 定义文法并解析 AT 指令解析器、简单表达式计算
迭代器 统一遍历方式 链表、环形缓冲区、消息队列遍历
中介者 多对象交互集中到中介 事件中心、消息总线
备忘录 存档与恢复状态 参数掉电保存、配置回滚
观察者 一对多通知 传感器订阅、事件总线、UI 刷新
状态 状态封装成类,消除条件分支 协议状态机、按键消抖、充电流程
策略 算法可互换 滤波算法、编解码切换
模板方法 父类定骨架,子类填步骤 驱动初始化流程、外设抽象
访问者 不改元素加操作 设备树多维度遍历、协议统计

新手重点掌握:状态、观察者、策略、责任链、模板方法。这五个在嵌入式里出现频率最高。

配图:行为型模式分类树


1. 责任链模式(Chain of Responsibility)

是什么

把多个处理对象串成一条链,请求沿链传递,每个处理者决定"我处理"还是"传给下一个"。发送者不需要知道最终是谁处理的,实现发送者与接收者解耦。

用在哪

  • AT 指令解析:先判断是不是 OK,再判断 ERROR,再判断 +URC,最后默认处理
  • 日志系统:DEBUG 信息走串口,WARN 走文件,ERROR 触发告警
  • 按键事件:短按、长按、双击,沿链判断

配图:责任链结构与时序

C 代码:AT 响应解析链

c 复制代码
#include <stdint.h>
#include <string.h>
#include <stdbool.h>

/* 处理结果 */
typedef enum {
    CHAIN_PASS = 0,   /* 本节点不处理,传给下一个 */
    CHAIN_HANDLED     /* 已处理,停止传递 */
} ChainResult;

/* 责任链节点:函数指针 + next 指针 */
typedef struct ChainNode {
    ChainResult (*handle)(const char *line, void *ctx);
    struct ChainNode *next;
} ChainNode;

/* 责任链头部,注册后统一调用 */
void Chain_Dispatch(ChainNode *head, const char *line, void *ctx)
{
    ChainNode *node = head;
    while (node != NULL) {
        if (node->handle(line, ctx) == CHAIN_HANDLED) {
            break;   /* 已处理,停止传递 */
        }
        node = node->next;
    }
}

/* ---------- 具体处理者:解析 OK ---------- */
static ChainResult HandleOk(const char *line, void *ctx)
{
    (void)ctx;
    if (strcmp(line, "OK") == 0) {
        /* TODO: 唤醒等待 AT 响应的任务 */
        return CHAIN_HANDLED;
    }
    return CHAIN_PASS;
}

/* ---------- 具体处理者:解析 ERROR ---------- */
static ChainResult HandleError(const char *line, void *ctx)
{
    (void)ctx;
    if (strncmp(line, "ERROR", 5) == 0) {
        /* TODO: 设置错误标志,通知等待任务 */
        return CHAIN_HANDLED;
    }
    return CHAIN_PASS;
}

/* ---------- 具体处理者:解析 URC(主动上报) ---------- */
static ChainResult HandleUrc(const char *line, void *ctx)
{
    (void)ctx;
    if (line[0] == '+') {
        /* TODO: 分发到 URC 处理表 */
        return CHAIN_HANDLED;
    }
    return CHAIN_PASS;
}

/* ---------- 组装责任链 ---------- */
void AtParser_Init(void)
{
    static ChainNode nodeUrc  = { HandleUrc,  NULL };
    static ChainNode nodeErr  = { HandleError, &nodeUrc };
    static ChainNode nodeOk   = { HandleOk,    &nodeErr };

    /* 使用时:Chain_Dispatch(&nodeOk, line, ctx); */
    (void)nodeOk;
}

容易踩的坑

  • 链别太长。超过 5 个节点就该考虑用哈希表或分发表了,链式遍历有性能开销。
  • 顺序敏感 。把高频/快判断的放前面,比如 URC 判断 line[0] == '+' 代价极低,应靠前。
  • 避免循环链 。组装时小心 next 指针别指回头部。

2. 命令模式(Command)

是什么

把"一个请求/动作"封装成独立的对象(C 里就是结构体 + 函数指针),让请求可以排队、记录、撤销、重做。请求的发起者不需要知道执行者是谁。

用在哪

  • 按键事件队列:按键扫描任务把"按键 X 被按下"封装成命令丢队列,业务任务取出执行
  • 串口协议命令分发:每条协议指令封装成一个命令对象
  • 撤销/重做:参数配置工具的 Undo 栈

配图:命令模式类图

C 代码:按键命令队列

c 复制代码
#include <stdint.h>
#include <stdbool.h>

/* 命令接口:每个命令实现 execute */
typedef struct Command {
    void (*execute)(struct Command *self, void *arg);
    void (*undo)(struct Command *self);    /* 可选:撤销 */
    uint8_t cmd_id;                         /* 命令标识 */
} Command;

/* 简单环形队列 */
#define CMD_QUEUE_SIZE  16
static Command *s_queue[CMD_QUEUE_SIZE];
static volatile uint8_t s_head = 0;
static volatile uint8_t s_tail = 0;

bool CmdQueue_Push(Command *cmd)
{
    uint8_t next = (s_tail + 1u) % CMD_QUEUE_SIZE;
    if (next == s_head) {
        return false;   /* 队列满 */
    }
    s_queue[s_tail] = cmd;
    s_tail = next;
    return true;
}

Command *CmdQueue_Pop(void)
{
    if (s_head == s_tail) {
        return NULL;    /* 队列空 */
    }
    Command *cmd = s_queue[s_head];
    s_head = (s_head + 1u) % CMD_QUEUE_SIZE;
    return cmd;
}

/* ---------- 具体命令:切换 LED ---------- */
typedef struct {
    Command base;
    uint8_t led_id;
} LedCmd;

static void LedCmd_Execute(Command *self, void *arg)
{
    LedCmd *c = (LedCmd *)self;
    (void)arg;
    /* TODO: LED_Toggle(c->led_id); */
    (void)c;
}

/* 业务任务:从队列取命令并执行 */
void Task_CommandLoop(void *arg)
{
    (void)arg;
    for (;;) {
        Command *cmd = CmdQueue_Pop();
        if (cmd != NULL) {
            cmd->execute(cmd, NULL);
        } else {
            /* vTaskDelay(pdMS_TO_TICKS(10)); */
        }
    }
}

容易踩的坑

  • 命令对象的生命周期。如果命令是栈上临时变量,入队后函数返回就悬空了。要么用静态分配,要么用对象池。
  • 别把业务塞进命令。命令只负责"调用谁",具体逻辑在被调用方。命令里写几百行就是反模式。

3. 解释器模式(Interpreter)

是什么

针对一类特定"语言"(可以很简单,比如 SET 12 0xFF),定义文法规则,并实现一个解释器来解析执行。日常开发用得少,但在嵌入式里 AT 指令解析、串口调试命令行、Modbus 寄存器映射 都能看到它的影子。

用在哪

  • 串口调试 CLI(led onread 0x40021000
  • 简单表达式计算(temp * 0.01 - 40
  • AT 指令参数解析

配图:解释器结构

C 代码:简易串口 CLI 解释器

c 复制代码
#include <string.h>
#include <stdlib.h>

/* 命令表条目 */
typedef struct {
    const char *name;                                   /* 指令名 */
    void (*handler)(int argc, char *argv[]);            /* 处理函数 */
    const char *help;                                   /* 帮助文本 */
} CliCmd;

/* 简单 tokenizer:按空格切分 */
static int Tokenize(char *line, char *argv[], int max)
{
    int argc = 0;
    char *p = line;
    while (*p != '\0' && argc < max) {
        while (*p == ' ') { *p++ = '\0'; }   /* 跳过空格 */
        if (*p == '\0') { break; }
        argv[argc++] = p;
        while (*p != '\0' && *p != ' ') { p++; }
    }
    return argc;
}

/* 命令表 */
static const CliCmd s_cmds[] = {
    { "led",   Cmd_Led,   "led <on|off> <id>" },
    { "read",  Cmd_Read,  "read <addr>" },
    { "help",  Cmd_Help,  "show help" },
};

#define CMD_COUNT  (sizeof(s_cmds) / sizeof(s_cmds[0]))

/* 解释器入口 */
void Cli_Interpret(char *line)
{
    char *argv[8];
    int argc = Tokenize(line, argv, 8);
    if (argc == 0) { return; }

    for (uint32_t i = 0; i < CMD_COUNT; i++) {
        if (strcmp(argv[0], s_cmds[i].name) == 0) {
            s_cmds[i].handler(argc, argv);
            return;
        }
    }
    /* 未知指令 */
    /* TODO: 输出 "unknown command" */
}

/* 示例 handler */
void Cmd_Led(int argc, char *argv[])
{
    if (argc < 3) { return; }
    bool on = (strcmp(argv[1], "on") == 0);
    uint8_t id = (uint8_t)atoi(argv[2]);
    /* TODO: 控制 LED */
    (void)on; (void)id;
}

容易踩的坑

  • 别造复杂语言。如果语法开始出现嵌套括号、运算优先级,赶紧换 Lua 或现成解析器,C 手撸解释器是地狱。
  • Modbus 这种成熟协议别自己解释,用现成库(FreeModbus、libmodbus)。

4. 迭代器模式(Iterator)

是什么

提供统一的方式顺序访问一个聚合对象的元素,不暴露集合内部结构 。C 里没有 foreach,但可以用函数指针模拟迭代器接口。

用在哪

  • 链表遍历
  • 环形缓冲区遍历
  • 消息队列消费
  • 设备链表枚举

配图:迭代器结构

C 代码:通用链表迭代器

c 复制代码
#include <stdbool.h>

/* 链表节点 */
typedef struct ListNode {
    struct ListNode *next;
    void *data;     /* 节点持有任意类型数据 */
} ListNode;

/* 迭代器:持有当前节点指针 */
typedef struct {
    ListNode *cur;
} ListIter;

void ListIter_Init(ListIter *it, ListNode *head) { it->cur = head; }

bool ListIter_HasNext(ListIter *it) { return it->cur != NULL; }

void *ListIter_Next(ListIter *it)
{
    if (it->cur == NULL) { return NULL; }
    void *data = it->cur->data;
    it->cur = it->cur->next;
    return data;
}

/* 使用示例:遍历设备链表,统一调用 Init */
void Bus_InitAllDevices(ListNode *head)
{
    ListIter it;
    ListIter_Init(&it, head);
    while (ListIter_HasNext(&it)) {
        Device *dev = (Device *)ListIter_Next(&it);
        if (dev != NULL && dev->init != NULL) {
            dev->init(dev);
        }
    }
}

容易踩的坑

  • 遍历中删除节点 。标准做法是迭代器返回"待删除标记",遍历结束后统一删除;或用 prev 指针谨慎处理。
  • 多线程遍历要加锁。一个任务遍历、另一个任务插入节点,必崩。

5. 中介者模式(Mediator)

是什么

用中介对象封装多个对象之间的交互,把"网状依赖"变成"星型依赖"。所有对象只跟中介通信,互不直接引用。

用在哪

  • 事件中心 / 消息总线:模块 A 触发事件,模块 B、C 订阅,互不感知
  • 任务间协调:采集任务、上传任务、显示任务通过中介者协调
  • UI 与业务解耦:UI 不直接调业务,通过中介者转发

配图:中介者模式(网状 → 星型)

C 代码:事件总线中介者

c 复制代码
#include <stdint.h>

#define MAX_SUBSCRIBERS  8

/* 事件 ID 枚举 */
typedef enum {
    EVT_TEMP_UPDATED = 0,
    EVT_NET_CONNECTED,
    EVT_NET_DISCONNECTED,
    EVT_COUNT
} EventId;

/* 订阅者回调 */
typedef void (*EventHandler)(EventId id, void *payload, uint16_t len);

/* 中介者:每个事件维护一个订阅者数组 */
typedef struct {
    EventHandler subs[EVT_COUNT][MAX_SUBSCRIBERS];
    uint8_t      sub_count[EVT_COUNT];
} EventBus;

static EventBus s_bus;

/* 订阅 */
int8_t EventBus_Subscribe(EventId id, EventHandler handler)
{
    if (id >= EVT_COUNT) { return -1; }
    uint8_t cnt = s_bus.sub_count[id];
    if (cnt >= MAX_SUBSCRIBERS) { return -1; }
    s_bus.subs[id][cnt] = handler;
    s_bus.sub_count[id] = cnt + 1u;
    return 0;
}

/* 发布:中介者统一通知所有订阅者 */
void EventBus_Publish(EventId id, void *payload, uint16_t len)
{
    if (id >= EVT_COUNT) { return; }
    uint8_t cnt = s_bus.sub_count[id];
    for (uint8_t i = 0; i < cnt; i++) {
        if (s_bus.subs[id][i] != NULL) {
            s_bus.subs[id][i](id, payload, len);
        }
    }
}

/* ---------- 使用示例 ---------- */
/* 传感器任务:采集后发布 */
void Task_Sensor(void *arg)
{
    (void)arg;
    float temp = 25.6f;
    for (;;) {
        /* temp = ReadTemp(); */
        EventBus_Publish(EVT_TEMP_UPDATED, &temp, sizeof(temp));
        /* vTaskDelay(pdMS_TO_TICKS(1000)); */
        break;
    }
}

/* 显示模块:订阅温度事件 */
static void OnTempUpdated(EventId id, void *payload, uint16_t len)
{
    (void)id; (void)len;
    float temp = *(float *)payload;
    /* TODO: LCD_ShowTemp(temp); */
    (void)temp;
}

void App_Init(void)
{
    EventBus_Subscribe(EVT_TEMP_UPDATED, OnTempUpdated);
}

容易踩的坑

  • 回调里别做重活。中断里发布事件,订阅回调在中断上下文执行会拖垮实时性。推荐"中断发布 → 任务消费"的二段式。
  • 订阅者数量要有上限。无限增长会内存泄漏或越界,用静态数组 + 编译期上限。
  • 避免循环发布。A 订阅 B 的事件,回调里又发布一个事件触发 B,死循环。

6. 备忘录模式(Memento)

是什么

在不破坏封装的前提下,捕获对象的内部状态,保存到外部,以便日后恢复。嵌入式里最典型的应用就是参数掉电保存与上电恢复

用在哪

  • 系统参数掉电保存(Flash/EEPROM)
  • 配置回滚(OTA 失败恢复上一版本参数)
  • 表单草稿、断点续传

配图:备忘录保存与恢复时序

C 代码:参数备份与恢复

c 复制代码
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

/* 原发器:系统参数对象 */
typedef struct {
    int16_t  temp_threshold;     /* 告警阈值 */
    uint16_t sample_interval_ms; /* 采样周期 */
    uint8_t  brightness;         /* 屏幕亮度 */
} SysConfig;

/* 备忘录:保存到 Flash 的快照(加魔数和 CRC 校验) */
typedef struct {
    uint32_t magic;              /* 魔数:用于判断 Flash 是否有效 */
    SysConfig cfg;
    uint32_t crc;                /* CRC32 校验 */
} ConfigMemento;

/* 计算 CRC32(简化版) */
static uint32_t CalcCrc32(const uint8_t *data, uint32_t len)
{
    uint32_t crc = 0xFFFFFFFFu;
    for (uint32_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            crc = (crc >> 1) ^ (0xEDB88320u & -(crc & 1u));
        }
    }
    return ~crc;
}

#define CFG_MAGIC  0xCFGA5A5u

/* 创建备忘录:把当前配置打包成可持久化的快照 */
void Config_SaveMemento(const SysConfig *cfg, ConfigMemento *out)
{
    out->magic = CFG_MAGIC;
    memcpy(&out->cfg, cfg, sizeof(SysConfig));
    out->crc = CalcCrc32((const uint8_t *)cfg, sizeof(SysConfig));
}

/* 从备忘录恢复:校验失败返回 false */
bool Config_RestoreFromMemento(SysConfig *cfg, const ConfigMemento *m)
{
    if (m->magic != CFG_MAGIC) { return false; }
    uint32_t crc = CalcCrc32((const uint8_t *)&m->cfg, sizeof(SysConfig));
    if (crc != m->crc) { return false; }
    memcpy(cfg, &m->cfg, sizeof(SysConfig));
    return true;
}

/* 使用:上电时从 Flash 读取,校验失败则用默认值 */
void App_LoadConfig(SysConfig *cfg)
{
    ConfigMemento m;
    /* Flash_Read(CONFIG_ADDR, &m, sizeof(m)); */
    if (!Config_RestoreFromMemento(cfg, &m)) {
        /* 校验失败:使用默认值 */
        cfg->temp_threshold     = 500;     /* 50.0℃ */
        cfg->sample_interval_ms = 1000;
        cfg->brightness         = 80;
    }
}

容易踩的坑

  • 必须有魔数 + CRC。Flash 掉电写一半、首次上电全 0xFF,没有校验就拿到一堆垃圾值。
  • 版本兼容 。结构体一改,老设备的 Flash 数据就废了。加 version 字段,迁移时按版本升级。
  • Flash 写入前先擦除。NOR Flash 只能 1→0,写前必须 Sector Erase。

7. 观察者模式(Observer)

是什么

定义对象间一对多依赖:当一个对象状态变化时,所有依赖它的对象都自动收到通知。也叫"发布-订阅"。其实第 5 节的事件总线就是一种观察者模式的实现------这里再给一个更直接的"主题-观察者"版本。

用在哪

  • 传感器数据订阅(一个采集源,多个消费者:显示、上传、告警)
  • UI 数据绑定
  • 系统状态变化通知

配图:观察者模式类图

C 代码:温度采集主题 + 多观察者

c 复制代码
#include <stdint.h>

/* 观察者接口 */
typedef struct Observer {
    void (*on_update)(struct Observer *self, float value);
    struct Observer *next;     /* 链式订阅 */
} Observer;

/* 主题:温度采集器 */
typedef struct {
    Observer *head;            /* 订阅链表头 */
    float     current_temp;
} TempSubject;

void TempSubject_Attach(TempSubject *s, Observer *o)
{
    o->next = s->head;
    s->head = o;
}

/* 状态变化时通知所有观察者 */
void TempSubject_SetValue(TempSubject *s, float value)
{
    s->current_temp = value;
    Observer *o = s->head;
    while (o != NULL) {
        if (o->on_update != NULL) {
            o->on_update(o, value);
        }
        o = o->next;
    }
}

/* ---------- 三个具体观察者 ---------- */
typedef struct { Observer base; } DisplayObserver;
typedef struct { Observer base; } UploadObserver;
typedef struct { Observer base; } AlarmObserver;

static void Display_OnUpdate(Observer *self, float value)
{
    (void)self;
    /* TODO: LCD_ShowTemp(value); */
    (void)value;
}

static void Upload_OnUpdate(Observer *self, float value)
{
    (void)self;
    /* TODO: MQTT_Publish("temp", value); */
    (void)value;
}

static void Alarm_OnUpdate(Observer *self, float value)
{
    (void)self;
    if (value > 50.0f) {
        /* TODO: Buzzer_On(); */
    }
}

void App_SetupObservers(TempSubject *s)
{
    static DisplayObserver disp = { Display_OnUpdate, NULL };
    static UploadObserver  up   = { Upload_OnUpdate,  NULL };
    static AlarmObserver   alm  = { Alarm_OnUpdate,   NULL };

    TempSubject_Attach(s, &disp.base);
    TempSubject_Attach(s, &up.base);
    TempSubject_Attach(s, &alm.base);
}

容易踩的坑

  • 观察者与中介者的区别:观察者是"一个主题通知多个订阅者";中介者是"任意模块互相通信"。简单一对多用观察者,多对多用中介者。
  • 通知顺序不保证。如果观察者之间有依赖(比如告警必须在采集后),别靠链表顺序,用显式优先级或拆成两个事件。
  • 取消订阅。如果订阅者是临时对象,记得从链表摘除,否则下次通知调到野指针。

8. 状态模式(State)

是什么

把对象的不同状态封装成独立的状态对象,状态切换时行为自动跟着变,用多态替代巨大的 switch-case。嵌入式里协议状态机、按键状态机、充电状态机到处都是。

用在哪

  • TCP/Modbus/自定义协议状态机
  • 按键消抖状态机(按下、抖动、长按、释放)
  • 电池充电流程(待机、预充、恒流、恒压、满电)
  • 电梯、自动门控制系统

配图:按键状态机状态图

C 代码:按键状态机(带长按检测)

c 复制代码
#include <stdint.h>
#include <stdbool.h>

/* 状态机事件 */
typedef enum {
    EVT_KEY_DOWN = 0,
    EVT_KEY_UP,
    EVT_TICK_10MS
} KeyEvt;

/* 前置声明 */
typedef struct KeyFsm KeyFsm;

/* 状态接口:每个状态实现一组事件处理函数 */
typedef struct {
    void (*on_event)(KeyFsm *fsm, KeyEvt evt);
    const char *name;
} KeyState;

/* 状态机上下文 */
struct KeyFsm {
    const KeyState *cur;        /* 当前状态 */
    uint16_t       hold_ms;     /* 已经按下的毫秒数 */
    bool           long_pressed;
};

/* 切换状态 */
void KeyFsm_Transition(KeyFsm *fsm, const KeyState *next)
{
    fsm->cur = next;
    fsm->hold_ms = 0u;
}

/* ---------- 状态:空闲 ---------- */
static void StateIdle_OnEvent(KeyFsm *fsm, KeyEvt evt);

static const KeyState StateIdle = { StateIdle_OnEvent, "Idle" };

static void StateIdle_OnEvent(KeyFsm *fsm, KeyEvt evt)
{
    if (evt == EVT_KEY_DOWN) {
        /* 进入按下状态 */
        /* KeyFsm_Transition(fsm, &StatePressed); */
    }
}

/* ---------- 状态:按下(判断长按) ---------- */
static void StatePressed_OnEvent(KeyFsm *fsm, KeyEvt evt);

static const KeyState StatePressed = { StatePressed_OnEvent, "Pressed" };

static void StatePressed_OnEvent(KeyFsm *fsm, KeyEvt evt)
{
    switch (evt) {
        case EVT_TICK_10MS:
            fsm->hold_ms += 10u;
            if (fsm->hold_ms >= 1000u && !fsm->long_pressed) {
                fsm->long_pressed = true;
                /* TODO: 触发长按事件 LongPress_Callback(); */
            }
            break;

        case EVT_KEY_UP:
            if (!fsm->long_pressed) {
                /* TODO: 触发短按事件 ShortPress_Callback(); */
            }
            fsm->long_pressed = false;
            /* KeyFsm_Transition(fsm, &StateIdle); */
            break;

        default:
            break;
    }
}

/* 状态机初始化 */
void KeyFsm_Init(KeyFsm *fsm)
{
    fsm->cur          = &StateIdle;
    fsm->hold_ms      = 0u;
    fsm->long_pressed = false;
}

/* 外部事件入口 */
void KeyFsm_Handle(KeyFsm *fsm, KeyEvt evt)
{
    if (fsm->cur != NULL && fsm->cur->on_event != NULL) {
        fsm->cur->on_event(fsm, evt);
    }
}

容易踩的坑

  • 状态爆炸。状态超过 10 个就该考虑分层状态机(HSM),参考 QP/qpnano 框架。
  • 状态切换时别忘记清理上下文hold_ms 这种临时计数器,切换状态时必须清零,否则下次进来直接命中长按。
  • 每个状态都要处理所有事件。漏掉一个事件,状态机就卡死。建议用状态-事件矩阵表先画清楚再写代码。

经验 :复杂状态机强烈推荐画状态图。工具推荐用 PlantUMLdraw.io,画完再写代码,bug 少一半。


9. 策略模式(Strategy)

是什么

定义一系列可互换的算法,每个算法独立封装,运行时可灵活切换。算法的变化不影响使用方

用在哪

  • 滤波算法切换(移动平均、中值、卡尔曼)
  • 编解码切换(CRC8/CRC16/CRC32)
  • 不同传感器的换算公式
  • 排序算法选择

配图:策略模式类图

C 代码:温度滤波策略

c 复制代码
#include <stdint.h>

/* 策略接口 */
typedef struct {
    float (*filter)(struct FilterStrategy *self, float sample);
    void  (*reset)(struct FilterStrategy *self);
} FilterStrategy;

/* ---------- 策略 A:移动平均 ---------- */
#define MAVG_WIN  8
typedef struct {
    FilterStrategy base;
    float  buf[MAAVG_WIN];
    uint8_t idx;
    uint8_t count;
} MovingAvgFilter;

static float MovingAvg_Filter(FilterStrategy *self, float sample)
{
    MovingAvgFilter *f = (MovingAvgFilter *)self;
    f->buf[f->idx] = sample;
    f->idx = (f->idx + 1u) % MAVG_WIN;
    if (f->count < MAVG_WIN) { f->count++; }

    float sum = 0.0f;
    for (uint8_t i = 0; i < f->count; i++) {
        sum += f->buf[i];
    }
    return sum / (float)f->count;
}

static void MovingAvg_Reset(FilterStrategy *self)
{
    MovingAvgFilter *f = (MovingAvgFilter *)self;
    f->idx = 0u;
    f->count = 0u;
    for (uint8_t i = 0; i < MAVG_WIN; i++) { f->buf[i] = 0.0f; }
}

/* ---------- 策略 B:中值滤波 ---------- */
#define MED_WIN  5
typedef struct {
    FilterStrategy base;
    float  buf[MED_WIN];
    uint8_t count;
} MedianFilter;

static float Median_Filter(FilterStrategy *self, float sample)
{
    MedianFilter *f = (MedianFilter *)self;
    /* 滑动窗口:左移,最新值放末尾 */
    for (uint8_t i = 0; i < MED_WIN - 1u; i++) {
        f->buf[i] = f->buf[i + 1u];
    }
    f->buf[MED_WIN - 1u] = sample;
    if (f->count < MED_WIN) { f->count++; }

    /* 简单冒泡排序取中值(窗口小,够用) */
    float tmp[MED_WIN];
    for (uint8_t i = 0; i < f->count; i++) { tmp[i] = f->buf[i]; }
    for (uint8_t i = 0; i < f->count - 1u; i++) {
        for (uint8_t j = 0; j < f->count - 1u - i; j++) {
            if (tmp[j] > tmp[j + 1u]) {
                float t = tmp[j]; tmp[j] = tmp[j + 1u]; tmp[j + 1u] = t;
            }
        }
    }
    return tmp[f->count / 2u];
}

/* ---------- 使用方:温度采集器,运行时切换策略 ---------- */
typedef struct {
    FilterStrategy *strategy;   /* 持有当前策略 */
} TempSensor;

float TempSensor_Read(TempSensor *s)
{
    float raw = 0.0f;  /* TODO: ADC_Read(); */
    return s->strategy->filter(s->strategy, raw);
}

void TempSensor_SetStrategy(TempSensor *s, FilterStrategy *st)
{
    s->strategy = st;
}

容易踩的坑

  • 策略切换时记得 reset。换滤波器前不清空历史窗口,前一个策略的残留数据会污染新策略的输出。
  • 别为只有 2 个算法上策略模式 。一个 if-else 就够了,3 个及以上再上。
  • 策略对象别频繁创建销毁。用静态实例,运行时只切换指针。

10. 模板方法模式(Template Method)

是什么

在"父类"中定义算法的整体骨架,把可变步骤延迟到"子类"实现。C 没有继承,但可以用 函数指针 + 结构体组合 模拟:骨架函数定义流程顺序,具体步骤通过回调注入。

用在哪

  • 驱动初始化流程(开时钟 → 配置 GPIO → 配置外设 → 使能中断)
  • 传感器采集流程(复位 → 配置 → 触发转换 → 读取 → 换算)
  • 通信协议流程(连接 → 握手 → 收发 → 断开)

配图:模板方法流程时序

C 代码:传感器驱动模板

c 复制代码
#include <stdint.h>
#include <stdbool.h>

/* 抽象步骤:每个具体传感器实现这组回调 */
typedef struct {
    bool (*reset)(void);
    void (*config)(uint8_t mode);
    void (*trigger)(void);
    uint16_t (*read_raw)(void);
    float  (*convert)(uint16_t raw);
    const char *name;
} SensorOps;

/* 模板方法:定义统一的采集流程骨架,子步骤由 ops 决定 */
bool Sensor_TemplateAcquire(const SensorOps *ops, float *out)
{
    if (ops == NULL || out == NULL) { return false; }

    /* 1. 复位 */
    if (ops->reset != NULL && !ops->reset()) {
        return false;
    }

    /* 2. 配置 */
    if (ops->config != NULL) {
        ops->config(0u);
    }

    /* 3. 触发转换 */
    if (ops->trigger != NULL) {
        ops->trigger();
    }

    /* 4. 读取原始值 */
    uint16_t raw = 0u;
    if (ops->read_raw != NULL) {
        raw = ops->read_raw();
    }

    /* 5. 换算 */
    if (ops->convert != NULL) {
        *out = ops->convert(raw);
    } else {
        *out = (float)raw;
    }

    return true;
}

/* ---------- 具体传感器 A:DS18B20 ---------- */
static bool Ds_Reset(void)        { /* TODO: 1-Wire 复位 */ return true; }
static void Ds_Config(uint8_t m)  { (void)m; /* 配置精度 */ }
static void Ds_Trigger(void)      { /* 触发温度转换 */ }
static uint16_t Ds_ReadRaw(void)  { return 0x01FFu; /* 示例 */ }
static float Ds_Convert(uint16_t r) { return (float)r * 0.0625f; }

static const SensorOps g_ds18b20 = {
    Ds_Reset, Ds_Config, Ds_Trigger, Ds_ReadRaw, Ds_Convert, "DS18B20"
};

/* ---------- 具体传感器 B:内部 ADC ---------- */
static bool Adc_Reset(void)        { return true; }
static void Adc_Config(uint8_t m)  { (void)m; /* ADC 通道配置 */ }
static void Adc_Trigger(void)      { /* 启动转换 */ }
static uint16_t Adc_ReadRaw(void)  { return 0x0800u; /* 示例 */ }
static float Adc_Convert(uint16_t r) {
    return (float)r * 3.3f / 4095.0f;   /* 12 位 ADC,3.3V 参考 */
}

static const SensorOps g_adc = {
    Adc_Reset, Adc_Config, Adc_Trigger, Adc_ReadRaw, Adc_Convert, "ADC"
};

/* 使用:同一套流程,不同传感器 */
void App_Demo(void)
{
    float temp, voltage;
    Sensor_TemplateAcquire(&g_ds18b20, &temp);
    Sensor_TemplateAcquire(&g_adc,     &voltage);
}

容易踩的坑

  • 骨架别太长。模板方法超过 30 行,说明流程本身有问题,应该拆分。
  • 回调要做 NULL 检查 。某个步骤可选时,必须 if (ops->step != NULL) 保护,否则裸指针调用直接 HardFault。
  • 流程顺序是约定。模板方法的核心价值就是"流程不可变",使用方不能跳过某步------所以骨架函数不能给使用方重写的机会。

11. 访问者模式(Visitor)

是什么

把"数据结构"和"操作"分离,在不修改元素类的前提下,为元素新增操作。适用于数据结构稳定、但操作频繁扩展的场景。

嵌入式里用得相对少,但 设备树多维度统计、协议字段多格式导出、AST 遍历 这类场景能看到。C 实现比较绕,因为要做"双重分派",举个轻量例子。

用在哪

  • 设备树多维度遍历(统计数量、打印信息、生成报告)
  • 协议字段导出(JSON、二进制、文本多种格式)
  • 调试器多视图展示

配图:访问者双重分派时序

C 代码:设备列表多操作访问

c 复制代码
#include <stdint.h>

/* 前置声明 */
typedef struct DeviceNode DeviceNode;
typedef struct DeviceVisitor DeviceVisitor;

/* 设备类型枚举 */
typedef enum {
    DEV_SENSOR = 0,
    DEV_ACTUATOR,
    DEV_COMM,
} DevType;

/* 设备节点(元素) */
struct DeviceNode {
    DevType type;
    const char *name;
    DeviceNode *next;
    /* 双重分派:把 visitor 传给自己,再让 visitor 调用自己的具体方法 */
    void (*accept)(DeviceNode *self, DeviceVisitor *v);
};

/* 访问者接口:每种设备类型一个访问方法 */
struct DeviceVisitor {
    void (*visit_sensor)(DeviceVisitor *v, DeviceNode *d);
    void (*visit_actuator)(DeviceVisitor *v, DeviceNode *d);
    void (*visit_comm)(DeviceVisitor *v, DeviceNode *d);
    void *ctx;     /* 访问者私有上下文,比如统计计数器 */
};

/* 各类型 accept 实现 */
static void Sensor_Accept(DeviceNode *self, DeviceVisitor *v)
{
    if (v->visit_sensor != NULL) { v->visit_sensor(v, self); }
}

static void Actuator_Accept(DeviceNode *self, DeviceVisitor *v)
{
    if (v->visit_actuator != NULL) { v->visit_actuator(v, self); }
}

static void Comm_Accept(DeviceNode *self, DeviceVisitor *v)
{
    if (v->visit_comm != NULL) { v->visit_comm(v, self); }
}

/* ---------- 具体访问者 A:统计设备数量 ---------- */
typedef struct {
    DeviceVisitor base;
    uint16_t sensor_cnt;
    uint16_t actuator_cnt;
    uint16_t comm_cnt;
} CountVisitor;

static void Count_Sensor(DeviceVisitor *v, DeviceNode *d)
{
    (void)d;
    CountVisitor *cv = (CountVisitor *)v;
    cv->sensor_cnt++;
}

static void Count_Actuator(DeviceVisitor *v, DeviceNode *d)
{
    (void)d;
    CountVisitor *cv = (CountVisitor *)v;
    cv->actuator_cnt++;
}

static void Count_Comm(DeviceVisitor *v, DeviceNode *d)
{
    (void)d;
    CountVisitor *cv = (CountVisitor *)v;
    cv->comm_cnt++;
}

/* ---------- 具体访问者 B:打印设备信息 ---------- */
static void Print_Sensor(DeviceVisitor *v, DeviceNode *d)
{
    (void)v;
    /* Log_Debug("[Sensor] %s", d->name); */
    (void)d;
}

static void Print_Actuator(DeviceVisitor *v, DeviceNode *d)
{
    (void)v;
    /* Log_Debug("[Actuator] %s", d->name); */
    (void)d;
}

static void Print_Comm(DeviceVisitor *v, DeviceNode *d)
{
    (void)v;
    /* Log_Debug("[Comm] %s", d->name); */
    (void)d;
}

/* 遍历设备链表,对每个节点执行 accept */
void DeviceList_Traverse(DeviceNode *head, DeviceVisitor *v)
{
    DeviceNode *p = head;
    while (p != NULL) {
        if (p->accept != NULL) {
            p->accept(p, v);
        }
        p = p->next;
    }
}

容易踩的坑

  • 数据结构频繁变化时别用 。新增一种设备类型,所有访问者都得改------这是访问者模式最大的代价。结构稳定、操作多变才用,反之用别的方式。
  • C 里双重分派写起来啰嗦 。评估收益:如果操作种类不多(≤2 种),直接在每个节点写 print()count() 方法更简单。
  • 优先考虑用函数指针表替代 。如果设备类型用枚举,可以用 func_table[DEV_TYPE] 直接查表,比双重分派轻量得多。

总结:行为型模式怎么选型?

速查表:

问题 推荐模式
一个事件多个模块关心 观察者 (一对一多) / 中介者(多对多)
协议解析、AT 响应处理 责任链
状态多、switch-case 巨长 状态模式
算法需要运行时切换 策略
流程固定,步骤可变 模板方法
按键 / 命令需要排队 命令
集合遍历要统一接口 迭代器
参数需要掉电保存 备忘录
自定义小语言解析 解释器(慎用)
数据结构稳定,操作多变 访问者(慎用)
  1. 状态机和观察者是嵌入式工程师的"看家模式",先吃透这两个,能解决一半以上的交互问题。
  2. 解释器和访问者在嵌入式里用得少,别强求,遇到再学。
  3. C 语言实现模式要"神似"而非"形似"。没有类就用结构体 + 函数指针,没有继承就用组合,没有多态就用 vtable。重点是思想,不是语法糖。
  4. 模式是工具不是 KPI 。一个三层菜单你给它套组合 + 装饰器 + 访问者,那不是工程,是炫技。简单优先,痛了再上模式

好的代码,不是用满了 23 种模式,而是看起来"平平无奇"------每层各管各的,改一个功能不用满世界找。模式只是达成这个目标的工具,不是目标本身


参考链接