装饰器模式:函数包装、功能增强、嵌套调用

说实话,装饰器模式这个名字听起来挺唬人的。我第一次接触它是在做嵌入式通信协议栈的时候,当时需要给数据包加校验、加密、日志,又不能改底层收发函数。嗯,说白了,装饰器就是给函数“穿衣服”——一层套一层,每层加点新功能。

在C语言里,我们没有面向对象的继承和多态,但函数指针给了我们另一种可能。你可以把函数当作参数传来传去,在调用前后插入自己的逻辑。这就是装饰器的核心思想。

什么是装饰器模式?

装饰器模式,本质上是一种不修改原函数,却能增强其功能的技巧。你想想看,项目做到一半,突然要加个超时重传、加个数据校验,如果直接改底层函数,风险太大了。我个人的习惯是,用装饰器在外面包一层,既安全又灵活。

核心三要素:

  • 原始函数:真正干活的函数,比如发送数据、读取传感器
  • 装饰函数:接收原始函数指针,在调用前后插入额外逻辑
  • 嵌套调用:多个装饰函数可以层层包裹,形成调用链

我在项目中遇到过这样一个场景:一个串口发送函数,需要先加CRC校验,再加密,最后加时间戳日志。如果把这些逻辑全塞进一个函数里,代码会变得又臭又长。用装饰器模式,每个功能独立成一个包装函数,想用哪个就套哪个,清爽多了。

函数指针:装饰器的基石

要实现装饰器,你得先理解函数指针。C语言里,函数名本身就是地址,你可以把它存起来、传出去。

// 定义一个函数类型
typedef int (*send_func_t)(const uint8_t *data, uint32_t len);

// 原始发送函数
int raw_send(const uint8_t *data, uint32_t len) {
    // 真正的硬件发送逻辑
    return HAL_UART_Transmit(&huart1, data, len, 1000);
}

// 装饰器:加CRC校验
int crc_decorator(send_func_t original, const uint8_t *data, uint32_t len) {
    uint8_t *buf = malloc(len + 2);
    memcpy(buf, data, len);
    uint16_t crc = calc_crc16(data, len);
    buf[len] = crc & 0xFF;
    buf[len+1] = (crc >> 8) & 0xFF;
    return original(buf, len + 2);
}

你看,crc_decorator 并没有修改 raw_send,只是在外面包了一层。原始函数该怎么写还怎么写,测试也不用重做。

嵌套调用:一层套一层

装饰器真正的威力在于嵌套。你可以把多个装饰函数串起来,像洋葱一样,一层剥开还有一层。

// 装饰器:加密
int encrypt_decorator(send_func_t original, const uint8_t *data, uint32_t len) {
    uint8_t *encrypted = aes_encrypt(data, len);
    int ret = original(encrypted, len);
    free(encrypted);
    return ret;
}

// 装饰器:日志
int log_decorator(send_func_t original, const uint8_t *data, uint32_t len) {
    printf("[LOG] Sending %u bytes\n", len);
    int ret = original(data, len);
    printf("[LOG] Sent, result=%d\n", ret);
    return ret;
}

// 使用:先加密,再CRC,最后发送
send_func_t wrapped = raw_send;
wrapped = crc_decorator(wrapped);   // 第一层:加CRC
wrapped = encrypt_decorator(wrapped); // 第二层:加密
wrapped = log_decorator(wrapped);    // 第三层:日志

wrapped(data, len);  // 实际调用顺序:日志 -> 加密 -> CRC -> raw_send

小技巧:嵌套顺序和调用顺序是相反的。最外层的装饰器最先执行,然后一层层往里走。我习惯把日志放在最外层,这样能记录到完整的入参和出参。

实战:传感器数据采集的装饰器链

我记得有一次做工业数据采集,需要从多个传感器读取温度值。原始读取函数很简单,但业务要求:超时重试、数据滤波、单位转换、异常报警。如果全写在一个函数里,维护起来简直是噩梦。

typedef float (*read_sensor_t)(int sensor_id);

// 原始读取
float raw_read(int sensor_id) {
    // 直接读取ADC寄存器
    return (float)adc_get_value(sensor_id);
}

// 装饰器:超时重试(最多3次)
float retry_decorator(read_sensor_t original, int sensor_id) {
    for (int i = 0; i < 3; i++) {
        float val = original(sensor_id);
        if (val > -273.0f) return val;  // 有效值判断
        delay_ms(10);
    }
    return -999.0f;  // 全部失败
}

// 装饰器:滑动平均滤波
float filter_decorator(read_sensor_t original, int sensor_id) {
    static float buf[5] = {0};
    static int idx = 0;
    buf[idx++ % 5] = original(sensor_id);
    float sum = 0;
    for (int i = 0; i < 5; i++) sum += buf[i];
    return sum / 5.0f;
}

// 装饰器:单位转换(摄氏度转华氏度)
float unit_decorator(read_sensor_t original, int sensor_id) {
    float celsius = original(sensor_id);
    return celsius * 9.0f / 5.0f + 32.0f;
}

// 装饰器:异常报警
float alarm_decorator(read_sensor_t original, int sensor_id) {
    float val = original(sensor_id);
    if (val > 100.0f || val < -40.0f) {
        printf("[ALARM] Sensor %d out of range: %.1f\n", sensor_id, val);
        // 触发蜂鸣器或LED
        HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);
    }
    return val;
}

注意:装饰器里如果用了静态变量(比如滤波器的缓冲区),多个传感器共用同一个缓冲区会出问题。我曾经踩过这个坑,排查了一整天。解决方案是给每个传感器分配独立的上下文结构体,或者用线程局部存储。

装饰器模式的适用场景

场景 说明 我的经验
协议栈分层 物理层、链路层、网络层各司其职 每个层都是一个装饰器,数据包逐层封装
中间件集成 日志、监控、鉴权等横切关注点 不侵入业务代码,随时可插拔
测试桩 模拟硬件行为,注入故障 用装饰器替换真实驱动,方便单元测试
性能统计 统计函数调用次数、耗时 包一层计时装饰器,不改原函数

装饰器模式的优缺点

优点:

  • 开闭原则:对扩展开放,对修改关闭。加功能不用改旧代码。
  • 灵活组合:想用哪个装饰器就用哪个,顺序还能调。
  • 职责单一:每个装饰器只做一件事,好理解也好测试。

缺点:

  • 嵌套过深时,调用链难以调试。我建议最多套3-4层,再多就该重构了。
  • 函数指针的间接调用会带来微小的性能开销。在嵌入式实时系统里,如果每秒调用上万次,这个开销不可忽略。
  • 类型安全性弱:函数指针类型必须严格匹配,否则编译器不会报错,运行时直接崩溃。

知识体系结构图

装饰器模式知识体系 装饰器模式 核心概念 原始函数(不修改) 装饰函数(包装增强) 嵌套调用(洋葱模型) 实现方式 函数指针作为参数 返回包装后的函数指针 链式调用组合 应用场景 协议栈分层 中间件集成 测试桩/性能统计

避坑指南

我曾经在一个项目里,把装饰器嵌套了7层。结果调试的时候,调用栈深得看不到底,一个bug查了三天。后来我定了个规矩:装饰器不超过4层。如果超过4层,说明你的设计可能有问题,该考虑用状态机或者责任链模式了。

另外,装饰器里如果分配了动态内存,一定要在返回前释放,否则内存泄漏等着你。我见过一个同事,在加密装饰器里malloc了缓冲区,但忘记free,结果系统跑了48小时就挂了。

我的建议:装饰器模式虽好,但别滥用。它最适合那些“横切关注点”——日志、鉴权、校验、重试。如果是业务逻辑本身的变化,用策略模式或者状态机更合适。


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