代码重构:坏味道识别,提取函数,消除重复,优化结构
说实话,我见过太多嵌入式项目死在「能跑就行」这句话上。代码能跑,不代表它健康。就像一个人能走路,不代表他身体没问题。今天咱们聊的代码重构,说白了就是给代码做一次体检加手术。
我个人习惯把重构看作一种投资。你花时间把代码理干净,后面调试、加功能、换平台,都会省下大把时间。我在项目中遇到过好几次,因为早期没重构,后期改一个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行,而且只在一个地方调用,留着它也没问题。
五、知识体系总览
下面这张图总结了代码重构的核心脉络,从识别坏味道到具体手法,再到最终目标:
六、重构的节奏与心态
重构不是一次性的「大扫除」。我建议把它融入日常开发:
- 小步提交:每次重构只改一个点,改完编译测试,没问题再继续。
- 测试先行:重构前先写好测试用例。没有测试的重构,就像蒙眼走钢丝。
- 不要边加功能边重构:这两件事分开做。加功能时只加功能,重构时只重构。
- 接受不完美:不是所有代码都要重构到完美。有些代码只跑一次,或者马上要废弃,留着它就好。
一句话总结:重构不是炫技,而是为了让下一个读你代码的人(包括三个月后的你自己)少骂两句。
你想想看,代码写出来是给人读的,顺便让机器执行。如果人读不懂,机器执行得再快也没用。下次你看到一段代码让你皱眉头,别忍着——动手重构它。