代码坏味道识别:过长函数、过大的类、重复代码、过度耦合

说实话,我见过太多嵌入式项目,代码能跑就行,但维护起来简直要命。今天咱们聊聊四个最常见的代码坏味道——过长函数、过大的类、重复代码、过度耦合。这些毛病,我几乎在每个项目里都遇到过。

一、过长函数:一个函数干太多事

什么叫过长函数?我个人的判断标准很简单:一个函数超过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。

五、如何系统性地识别这些坏味道

说了这么多,怎么在实际项目中快速发现这些问题?我总结了一套方法:

  1. 代码审查时重点关注:看函数长度、类的大小、重复代码块、模块间的依赖关系
  2. 使用静态分析工具:比如PC-Lint、Cppcheck,能自动检测过长函数和重复代码
  3. 画依赖图:把模块之间的调用关系画出来,一眼就能看出耦合程度
  4. 问自己三个问题:
    • 这个函数能独立测试吗?
    • 改这个模块会影响多少个其他模块?
    • 这段代码我见过几次了?

特别提醒:不要为了重构而重构。如果代码虽然有点长,但功能稳定、没有bug、短期内不会改动,那就先放着。优先处理那些「经常要改」或者「已经出过bug」的坏味道。

好了,这四种坏味道,你可以在自己的项目里对照检查一下。说实话,每个项目或多或少都有这些问题,关键是要有意识地去发现、去改进。下一章,咱们聊聊如何用设计模式来解决这些坏味道。