责任链模式:链表式处理、动态处理链、日志级别过滤

责任链模式,说白了就是让多个处理对象排成一条链,请求沿着链传递,直到有人处理它。我最早接触这个模式是在做嵌入式日志系统的时候——那时候我还在想,为什么不能把日志过滤、格式化、输出这些步骤串起来?后来发现,责任链模式就是干这个的。

为什么需要责任链?

你想想看,在嵌入式系统里,一个事件往往需要经过多个处理环节。比如一个按键事件:先要去抖,再判断长按短按,然后映射成功能码,最后执行动作。如果把这些逻辑全写在一个函数里,那代码会变成什么样?

我曾经接手过一个项目,一个按键处理函数写了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;
    }
}

注意:如果处理函数里分配了动态内存,记得在节点销毁前先释放。否则内存泄漏会慢慢吃掉你的堆空间——在嵌入式系统里,这往往意味着系统崩溃。

责任链模式的知识体系

下面这张图展示了责任链模式的核心结构和工作流程:

责任链模式结构图 请求进入 节点1:级别过滤 检查日志级别,决定是否处理 节点2:格式化 添加时间戳、标签等 节点3:输出 串口/文件/网络输出 处理完成 动态特性 • 节点可动态添加/删除 • 运行时调整处理顺序 • 每个节点独立测试 • 支持全链/单点模式 • 级别过滤灵活配置 • 内存管理需谨慎 • 适合日志/事件/数据流 • 避免循环引用

实际项目中的应用场景

除了日志系统,责任链模式在嵌入式领域还有很多用武之地:

应用场景 链节点示例 说明
按键事件处理 去抖 → 按键映射 → 功能执行 每个节点处理一个环节,可独立修改
数据包解析 校验 → 解包 → 协议转换 → 分发 不同协议可插入不同解析节点
传感器数据流 滤波 → 校准 → 单位转换 → 存储 每个传感器可配置不同的处理链
命令解析器 命令识别 → 参数解析 → 权限检查 → 执行 支持动态添加新命令

我个人最喜欢责任链模式的一点是:它让代码的扩展性变得特别好。新加一个功能,只需要写一个新的处理节点,然后把它挂到链上就行。不需要修改已有的代码,完全符合开闭原则。

嗯,最后说一句:责任链模式虽然好用,但也不要滥用。如果链太长(超过10个节点),调试起来会比较麻烦。我一般控制在5-7个节点以内,超过这个数就会考虑是否要拆分成多条链。


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