代码坏味道识别:过长函数、过大的类、重复代码、过度耦合
说实话,我见过太多嵌入式项目,代码能跑就行,但维护起来简直要命。今天咱们聊聊四个最常见的代码坏味道——过长函数、过大的类、重复代码、过度耦合。这些毛病,我几乎在每个项目里都遇到过。
一、过长函数:一个函数干太多事
什么叫过长函数?我个人的判断标准很简单:一个函数超过20行,你就得想想能不能拆。当然,这不是死规矩。但如果你一个函数写了上百行,那肯定有问题。
典型症状:
- 函数名很难起,因为功能太杂
- 需要大量注释来解释逻辑
- 嵌套if-else超过三层
- 局部变量超过10个
我在项目中遇到过这样一个案例:一个ADC采集函数,里面包含了初始化、滤波、校准、数据转换、甚至还有日志打印。你想想看,这五个功能揉在一起,改一个地方就可能影响其他逻辑。
// 坏味道:过长函数
void adc_process(void) {
// 初始化ADC
adc_init();
adc_set_channel(0);
adc_start();
// 等待转换完成
while(!adc_is_done());
// 读取原始值
uint16_t raw = adc_read();
// 中值滤波
static uint16_t buf[5];
static uint8_t idx = 0;
buf[idx++] = raw;
if(idx >= 5) idx = 0;
// ... 排序取中值 ...
// 校准
raw = raw * calib_factor / 1000;
// 转换成电压
float voltage = raw * 3.3f / 4096.0f;
// 打印日志
printf("ADC: %d mV\n", (int)(voltage * 1000));
// 更新全局变量
g_adc_value = voltage;
}
我的建议:每个函数只做一件事。上面这个函数,至少可以拆成5个:adc_read_raw()、median_filter()、calibrate()、convert_to_voltage()、log_adc_value()。每个函数职责清晰,测试也方便。
二、过大的类:一个类管太多
嵌入式C++项目里,过大的类特别常见。说白了,就是一个类承担了太多职责。我见过一个驱动类,里面既有硬件操作,又有数据处理,还有状态管理,甚至包含了UI逻辑。
为什么会这样?很多时候是因为偷懒。觉得「反正都是这个模块的,放一起方便」。但等你需要复用某个功能时,就傻眼了——你不得不把整个类都搬过去。
我曾经踩过的坑:一个通信协议解析类,里面包含了CRC校验、帧解析、超时管理、重传机制、还有日志记录。后来要移植到另一个平台,发现日志部分依赖了文件系统,而新平台没有。最后不得不把整个类重写。如果当初拆成ProtocolParser、CrcCalculator、TimeoutManager、RetransmissionHandler、Logger五个类,移植就简单多了。
判断类是否过大,有个简单方法:数一数这个类的成员函数数量。超过20个,基本就是过大了。超过30个,赶紧重构。
三、重复代码:复制粘贴的代价
重复代码,是嵌入式项目里最隐蔽的坏味道。为什么?因为嵌入式开发经常要操作寄存器,不同外设的初始化代码长得特别像。很多人图省事,直接复制粘贴改一改。
嗯,这里要注意:重复代码不只是浪费空间,更可怕的是维护灾难。你想想看,如果某个bug在10个地方都存在,你改漏了一个怎么办?
// 坏味道:重复代码
void uart1_init(void) {
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
USART1->BRR = 72000000 / 115200;
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
void uart2_init(void) {
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
USART2->BRR = 72000000 / 115200;
USART2->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
// 重构后:提取公共函数
void uart_init(USART_TypeDef *uart, uint32_t clk, uint32_t baud) {
uart->BRR = clk / baud;
uart->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
void uart1_init(void) {
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
uart_init(USART1, 72000000, 115200);
}
void uart2_init(void) {
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
uart_init(USART2, 72000000, 115200);
}
识别重复代码的技巧:
- 看到连续三行以上相同的代码,就要警惕
- 函数名不同但函数体几乎一样,肯定有问题
- 宏定义里出现重复的表达式,考虑用函数替代
四、过度耦合:牵一发而动全身
过度耦合,是嵌入式系统里最要命的坏味道。说白了就是模块之间依赖太深,改一个地方要连带改一堆。我见过最夸张的项目,一个全局变量被20个函数直接读写,谁都不敢动它。
嵌入式系统里,过度耦合的典型表现:
| 耦合类型 | 表现 | 危害 |
|---|---|---|
| 全局变量耦合 | 多个模块直接读写全局变量 | 无法独立测试,修改风险大 |
| 函数调用耦合 | A函数直接调用B函数的内部细节 | B改了,A也得改 |
| 数据结构耦合 | 模块之间共享复杂的数据结构 | 数据结构一变,所有模块都得改 |
| 时序耦合 | 模块A必须在模块B之后执行 | 调度顺序一乱,系统就崩 |
我的经验:降低耦合最有效的方法是「接口隔离」。每个模块只暴露必要的接口,内部实现完全隐藏。比如驱动层只提供init()、read()、write()三个函数,上层根本不需要知道底层用的是SPI还是I2C。
五、如何系统性地识别这些坏味道
说了这么多,怎么在实际项目中快速发现这些问题?我总结了一套方法:
- 代码审查时重点关注:看函数长度、类的大小、重复代码块、模块间的依赖关系
- 使用静态分析工具:比如PC-Lint、Cppcheck,能自动检测过长函数和重复代码
- 画依赖图:把模块之间的调用关系画出来,一眼就能看出耦合程度
- 问自己三个问题:
- 这个函数能独立测试吗?
- 改这个模块会影响多少个其他模块?
- 这段代码我见过几次了?
特别提醒:不要为了重构而重构。如果代码虽然有点长,但功能稳定、没有bug、短期内不会改动,那就先放着。优先处理那些「经常要改」或者「已经出过bug」的坏味道。
好了,这四种坏味道,你可以在自己的项目里对照检查一下。说实话,每个项目或多或少都有这些问题,关键是要有意识地去发现、去改进。下一章,咱们聊聊如何用设计模式来解决这些坏味道。