责任链模式:链表式处理、动态处理链、日志级别过滤
责任链模式,说白了就是让多个处理对象排成一条链,请求沿着链传递,直到有人处理它。我最早接触这个模式是在做嵌入式日志系统的时候——那时候我还在想,为什么不能把日志过滤、格式化、输出这些步骤串起来?后来发现,责任链模式就是干这个的。
为什么需要责任链?
你想想看,在嵌入式系统里,一个事件往往需要经过多个处理环节。比如一个按键事件:先要去抖,再判断长按短按,然后映射成功能码,最后执行动作。如果把这些逻辑全写在一个函数里,那代码会变成什么样?
我曾经接手过一个项目,一个按键处理函数写了800多行,里面全是if-else嵌套。每次加一个新功能,都要小心翼翼地在某个if分支里再插一段代码。嗯,这种代码我称之为「定时炸弹」——改一次炸一次。
责任链模式的核心思想就是:每个处理节点只关心自己该做的事,处理完了就传给下一个。这样每个节点都是独立的,可以单独测试、单独修改。
链表式处理结构
在C语言里实现责任链,最自然的方式就是用链表。每个节点是一个结构体,包含一个处理函数指针和一个指向下一个节点的指针。
// 责任链节点结构体
typedef struct ChainNode {
int level; // 处理级别,用于过滤
int (*handler)(void *data, int level); // 处理函数
struct ChainNode *next; // 指向下一个节点
} ChainNode_t;
// 创建节点
ChainNode_t* node_create(int level, int (*handler)(void*, int)) {
ChainNode_t *node = (ChainNode_t*)malloc(sizeof(ChainNode_t));
if (node) {
node->level = level;
node->handler = handler;
node->next = NULL;
}
return node;
}
// 将节点添加到链尾
void chain_append(ChainNode_t **head, ChainNode_t *node) {
if (*head == NULL) {
*head = node;
return;
}
ChainNode_t *cur = *head;
while (cur->next) {
cur = cur->next;
}
cur->next = node;
}
这个结构很简单,但很实用。每个节点有一个level字段,用来做级别过滤——比如日志系统里,只有级别匹配的日志才会被处理。
动态处理链:运行时组装
责任链最大的优势就是动态性。你可以在程序运行时添加、删除、重新排列节点。这在嵌入式系统里特别有用——比如根据不同的运行模式,加载不同的处理链。
我记得有个项目是做工业数据采集的,现场有几十种传感器,每种传感器的数据处理流程都不一样。如果用硬编码的方式,那代码量会爆炸。后来我用责任链模式,每种传感器对应一条处理链,运行时根据传感器类型动态组装。代码量减少了60%,而且新加传感器只需要写几个处理节点,然后拼成一条链就行。
// 动态插入节点到指定位置
int chain_insert(ChainNode_t **head, ChainNode_t *node, int position) {
if (position == 0) {
node->next = *head;
*head = node;
return 0;
}
ChainNode_t *cur = *head;
int i = 0;
while (cur && i < position - 1) {
cur = cur->next;
i++;
}
if (!cur) return -1; // 位置无效
node->next = cur->next;
cur->next = node;
return 0;
}
// 删除指定级别的节点
void chain_remove_by_level(ChainNode_t **head, int level) {
ChainNode_t *cur = *head;
ChainNode_t *prev = NULL;
while (cur) {
if (cur->level == level) {
if (prev) {
prev->next = cur->next;
} else {
*head = cur->next;
}
ChainNode_t *tmp = cur;
cur = cur->next;
free(tmp);
} else {
prev = cur;
cur = cur->next;
}
}
}
你看,插入和删除操作都是O(n)的,对于嵌入式系统来说完全够用。而且这种动态性让系统变得非常灵活——你可以在运行时根据配置加载不同的处理链,甚至可以在线更新处理逻辑。
日志级别过滤:最经典的应用
日志系统是责任链模式最经典的应用场景。每个日志消息都有一个级别(DEBUG、INFO、WARN、ERROR),不同的输出目标(串口、文件、网络)只关心特定级别的日志。
// 日志级别定义
#define LOG_LEVEL_DEBUG 0
#define LOG_LEVEL_INFO 1
#define LOG_LEVEL_WARN 2
#define LOG_LEVEL_ERROR 3
// 日志处理节点示例:串口输出
int uart_log_handler(void *data, int level) {
LogData_t *log = (LogData_t*)data;
if (level <= LOG_LEVEL_WARN) { // 只输出WARN及以下级别
printf("[UART] %s: %s\n", log->tag, log->message);
return 1; // 表示已处理
}
return 0; // 不处理,传给下一个
}
// 日志处理节点示例:文件记录
int file_log_handler(void *data, int level) {
LogData_t *log = (LogData_t*)data;
if (level >= LOG_LEVEL_ERROR) { // 只记录ERROR及以上级别
// 写入文件...
return 1;
}
return 0;
}
// 构建日志处理链
ChainNode_t *log_chain = NULL;
chain_append(&log_chain, node_create(LOG_LEVEL_DEBUG, uart_log_handler));
chain_append(&log_chain, node_create(LOG_LEVEL_INFO, uart_log_handler));
chain_append(&log_chain, node_create(LOG_LEVEL_ERROR, file_log_handler));
// 处理日志
void log_process(LogData_t *log, int level) {
ChainNode_t *cur = log_chain;
while (cur) {
if (cur->handler(log, level)) {
break; // 有人处理了就停止
}
cur = cur->next;
}
}
核心要点:每个节点只处理自己关心的级别,不关心的直接跳过。这样你可以灵活地控制哪些日志输出到哪里,而且可以随时添加新的输出方式。
责任链的变体:全链处理 vs 单点处理
在实际项目中,责任链有两种工作模式:
- 全链处理:请求经过所有节点,每个节点都可以处理。比如日志系统,一条日志可以同时输出到串口、文件、网络。
- 单点处理:请求被第一个匹配的节点处理后,就不再往下传。比如按键事件,被某个处理函数消费后就结束了。
这两种模式的区别就在于处理函数的返回值。全链模式下,处理函数返回0表示继续传递;单点模式下,返回非0表示停止传递。
我的建议:在设计责任链时,最好统一约定处理函数的返回值语义。我个人习惯用0表示「未处理,请继续」,非0表示「已处理,可停止」。这样代码的可读性会好很多。
避坑指南:我曾经踩过的坑
我曾经在一个项目里用责任链处理网络数据包,结果出了个诡异的bug——数据包被重复处理了。查了两天才发现,问题出在节点里忘了检查数据包是否已经被处理过。后来我在每个数据包里加了一个processed标志位,处理前先检查,问题就解决了。
还有一个常见的坑是内存泄漏。动态创建的责任链节点,在系统关闭或重新配置时一定要记得释放。我建议用一个专门的函数来销毁整条链:
void chain_destroy(ChainNode_t *head) {
ChainNode_t *cur = head;
while (cur) {
ChainNode_t *next = cur->next;
free(cur);
cur = next;
}
}
注意:如果处理函数里分配了动态内存,记得在节点销毁前先释放。否则内存泄漏会慢慢吃掉你的堆空间——在嵌入式系统里,这往往意味着系统崩溃。
责任链模式的知识体系
下面这张图展示了责任链模式的核心结构和工作流程:
实际项目中的应用场景
除了日志系统,责任链模式在嵌入式领域还有很多用武之地:
| 应用场景 | 链节点示例 | 说明 |
|---|---|---|
| 按键事件处理 | 去抖 → 按键映射 → 功能执行 | 每个节点处理一个环节,可独立修改 |
| 数据包解析 | 校验 → 解包 → 协议转换 → 分发 | 不同协议可插入不同解析节点 |
| 传感器数据流 | 滤波 → 校准 → 单位转换 → 存储 | 每个传感器可配置不同的处理链 |
| 命令解析器 | 命令识别 → 参数解析 → 权限检查 → 执行 | 支持动态添加新命令 |
我个人最喜欢责任链模式的一点是:它让代码的扩展性变得特别好。新加一个功能,只需要写一个新的处理节点,然后把它挂到链上就行。不需要修改已有的代码,完全符合开闭原则。
嗯,最后说一句:责任链模式虽然好用,但也不要滥用。如果链太长(超过10个节点),调试起来会比较麻烦。我一般控制在5-7个节点以内,超过这个数就会考虑是否要拆分成多条链。
公众号:蓝海资料掘金营,微信deep3321