装饰器模式:函数包装、功能增强、嵌套调用
说实话,装饰器模式这个名字听起来挺唬人的。我第一次接触它是在做嵌入式通信协议栈的时候,当时需要给数据包加校验、加密、日志,又不能改底层收发函数。嗯,说白了,装饰器就是给函数“穿衣服”——一层套一层,每层加点新功能。
在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小时就挂了。
我的建议:装饰器模式虽好,但别滥用。它最适合那些“横切关注点”——日志、鉴权、校验、重试。如果是业务逻辑本身的变化,用策略模式或者状态机更合适。