代码重构:坏味道识别,提取函数,消除重复,优化结构

说实话,我见过太多嵌入式项目死在「能跑就行」这句话上。代码能跑,不代表它健康。就像一个人能走路,不代表他身体没问题。今天咱们聊的代码重构,说白了就是给代码做一次体检加手术。

我个人习惯把重构看作一种投资。你花时间把代码理干净,后面调试、加功能、换平台,都会省下大把时间。我在项目中遇到过好几次,因为早期没重构,后期改一个bug引出三个新bug,最后整个模块重写。嗯,那滋味不好受。

一、坏味道识别:你的代码在求救

代码坏味道不是bug,但它预示着bug即将到来。我总结了嵌入式C代码中最常见的几种味道:

  • 重复代码:同样的逻辑出现在两处以上。这是最明显的信号。
  • 过长函数:一个函数超过50行,基本就该拆了。我见过一个函数写了400行,里面嵌套了6层if-else。
  • 过长的参数列表:参数超过4个,调用方容易搞错顺序。
  • 全局变量泛滥:全局变量到处飞,你根本不知道谁改了它。
  • 注释解释代码:如果注释在解释「这段代码在做什么」,说明代码本身不够清晰。
  • 散弹式修改:改一个需求,要改七八个文件。

核心判断标准:如果你看着一段代码,心里冒出「这写的什么玩意儿」的念头,那它就是坏味道。

二、提取函数:把大块肉切成小丁

提取函数是重构里最基础也最有效的手法。说白了,就是把一段逻辑独立出来,给它起个好名字。

举个例子,你可能会写出这样的代码:

// 重构前:一个函数干了三件事
void process_sensor_data(void) {
    uint32_t raw = read_adc();
    float voltage = raw * 3.3f / 4096.0f;
    float temp = (voltage - 0.5f) * 100.0f;
    
    if (temp > 85.0f) {
        set_alarm(ALARM_OVERHEAT);
        fan_on();
    }
    
    if (temp < -10.0f) {
        set_alarm(ALARM_FREEZE);
        heater_on();
    }
    
    log_temperature(temp);
    update_display(temp);
}

这段代码有什么问题?它把ADC读取、电压转换、温度计算、阈值判断、报警、风扇控制、日志、显示全部揉在一起。改任何一个逻辑,你都得读完整段代码。

重构之后:

// 重构后:每个函数只做一件事
void process_sensor_data(void) {
    float temp = read_temperature();
    handle_temperature_alarm(temp);
    update_user_interface(temp);
}

static float read_temperature(void) {
    uint32_t raw = read_adc();
    float voltage = adc_to_voltage(raw);
    return voltage_to_temperature(voltage);
}

static float adc_to_voltage(uint32_t raw) {
    return raw * 3.3f / 4096.0f;
}

static float voltage_to_temperature(float voltage) {
    return (voltage - 0.5f) * 100.0f;
}

static void handle_temperature_alarm(float temp) {
    if (temp > 85.0f) {
        set_alarm(ALARM_OVERHEAT);
        fan_on();
    }
    if (temp < -10.0f) {
        set_alarm(ALARM_FREEZE);
        heater_on();
    }
}

static void update_user_interface(float temp) {
    log_temperature(temp);
    update_display(temp);
}

我的经验:提取函数时,命名就是文档。函数名应该是一个动词短语,比如 read_temperature() 而不是 temp()。好的命名能省掉一半注释。

三、消除重复:DRY原则不是口号

DRY(Don't Repeat Yourself)是重构的核心目标之一。重复代码意味着:改一个地方,你得记得改所有地方。漏一个,bug就来了。

我曾经在一个项目中,发现同样的CRC校验代码在三个文件里各写了一遍。后来算法要升级,我改了第一个文件,忘了第二个和第三个。结果产品在客户现场跑了两周,数据校验全乱套。嗯,从那以后我对重复代码零容忍。

消除重复的常见手法:

  • 提取公共函数:相同的计算逻辑,放到一个函数里。
  • 使用宏或内联函数:对于非常短小的重复操作,比如寄存器位操作。
  • 合并相似函数:两个函数只有参数不同,考虑用参数化函数替代。
  • 提取常量:魔法数字重复出现时,定义成宏或枚举。
// 重复代码示例
void uart_send_byte(uint8_t data) {
    while (!(USART1->SR & USART_SR_TXE));
    USART1->DR = data;
}

void uart_send_string(const char *str) {
    while (*str) {
        while (!(USART1->SR & USART_SR_TXE));
        USART1->DR = *str++;
    }
}

// 重构后:消除等待发送的重复
static void uart_wait_tx_ready(void) {
    while (!(USART1->SR & USART_SR_TXE));
}

void uart_send_byte(uint8_t data) {
    uart_wait_tx_ready();
    USART1->DR = data;
}

void uart_send_string(const char *str) {
    while (*str) {
        uart_wait_tx_ready();
        USART1->DR = *str++;
    }
}

四、优化结构:让代码自己说话

结构优化是更高层次的重构。它关注的是模块之间的依赖关系、数据流向、职责划分。

我常用的结构优化手法:

  • 分层设计:驱动层、中间层、应用层各司其职。驱动层不关心业务逻辑,应用层不直接操作寄存器。
  • 减少耦合:模块之间通过接口通信,而不是直接访问对方的全局变量。
  • 内聚性提升:一个模块只做一类事情。比如「温度管理模块」只负责温度相关操作,不掺和LED闪烁。
  • 头文件瘦身:头文件只暴露必要的接口,内部实现细节藏起来。

避坑指南:我曾经为了「优化结构」,把一个简单的状态机拆成了五个文件,结果代码量翻了三倍,可读性反而下降了。重构不是越拆越好,适度才是关键。如果一个函数只有5行,而且只在一个地方调用,留着它也没问题。

五、知识体系总览

下面这张图总结了代码重构的核心脉络,从识别坏味道到具体手法,再到最终目标:

代码重构知识体系 第一步:识别坏味道 重复代码 | 过长函数 | 过长参数 | 全局变量泛滥 | 注释解释代码 | 散弹式修改 第二步:应用重构手法 提取函数 大函数拆小函数 消除重复 DRY原则 优化结构 分层·解耦·内聚 重命名 命名即文档 最终目标 可读性↑ 可维护性↑ 可测试性↑ 缺陷率↓

六、重构的节奏与心态

重构不是一次性的「大扫除」。我建议把它融入日常开发:

  • 小步提交:每次重构只改一个点,改完编译测试,没问题再继续。
  • 测试先行:重构前先写好测试用例。没有测试的重构,就像蒙眼走钢丝。
  • 不要边加功能边重构:这两件事分开做。加功能时只加功能,重构时只重构。
  • 接受不完美:不是所有代码都要重构到完美。有些代码只跑一次,或者马上要废弃,留着它就好。

一句话总结:重构不是炫技,而是为了让下一个读你代码的人(包括三个月后的你自己)少骂两句。

你想想看,代码写出来是给人读的,顺便让机器执行。如果人读不懂,机器执行得再快也没用。下次你看到一段代码让你皱眉头,别忍着——动手重构它。


专注资料整理