代码重构技术:提取函数、重命名、简化条件、重构工具使用
各位同学好,今天我们来聊聊代码重构。说实话,我见过太多嵌入式项目死在「能跑就别动」的魔咒里。代码越堆越乱,最后谁都不敢改。其实重构没那么玄乎,说白了就是给代码做「日常保洁」。
我自己的习惯是:每次提交代码前,花10分钟做一次小重构。别等到代码烂成一锅粥再动手,那时候成本就高了。今天我把最常用的几招分享给你。
一、提取函数:把大块头拆成小零件
这是最基础也最有效的重构手法。我在项目中遇到过一段300行的初始化函数,里面混杂着GPIO配置、定时器设置、中断注册、还有日志打印。每次改一个外设都得从头读到尾,生怕漏掉什么。
提取函数的核心原则:一个函数只做一件事。你想想看,如果一个函数既要做A又要做B,那它改起来就特别容易出问题。
重构前:
void system_init(void) {
// GPIO配置
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &gpio);
// 定时器配置
TIM_HandleTypeDef htim = {0};
htim.Instance = TIM2;
htim.Init.Period = 1000;
HAL_TIM_Base_Init(&htim);
// 中断注册
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
// 日志初始化
log_init();
log_info("System initialized");
}
你看,这个函数干了四件事。如果我想单独测试定时器,还得把整个函数跑一遍。重构后应该是这样:
重构后:
static void gpio_init(void) {
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &gpio);
}
static void timer_init(void) {
TIM_HandleTypeDef htim = {0};
htim.Instance = TIM2;
htim.Init.Period = 1000;
HAL_TIM_Base_Init(&htim);
}
static void nvic_init(void) {
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
void system_init(void) {
gpio_init();
timer_init();
nvic_init();
log_init();
log_info("System initialized");
}
嗯,这样清爽多了。每个函数职责单一,测试、调试、复用都方便。提取函数时有个小技巧:如果一个函数内部出现了注释块,那大概率可以拆出来。注释就是「这里应该是个独立函数」的信号。
二、重命名:让名字自己说话
我曾经接手过一个项目,变量名叫 a1、b2、tmp,函数名叫 do_stuff、process_data。你猜猜 process_data 是处理什么数据的?反正我猜了三天没猜对。
好的命名应该做到:看到名字就知道它是什么、干什么。不需要注释来解释变量名,那说明名字起得不好。
| 坏命名 | 好命名 | 说明 |
|---|---|---|
int a; |
int sensor_value; |
明确表示这是传感器读数 |
void calc(void); |
void calculate_crc(void); |
具体说明计算什么 |
#define MAX 100 |
#define MAX_BUFFER_SIZE 100 |
说明这个最大值属于谁 |
uint8_t flag; |
uint8_t is_connected; |
布尔变量用 is/has/can 开头 |
重命名时要注意:全局符号的改名要谨慎。我建议先用 IDE 的「重命名」功能,它会自动更新所有引用。手动替换容易漏掉,尤其是头文件里的声明。
我的命名习惯:
- 函数名用动词开头:
read_temperature()、send_packet() - 变量名用名词:
current_state、error_count - 宏定义全大写:
#define TIMEOUT_MS 5000 - 布尔变量用
is_、has_、can_开头
三、简化条件:让逻辑一目了然
条件判断是代码里最容易变复杂的地方。我见过这样的代码:
if ((status == OK) && (timeout > 0) && !(flag & ERROR_FLAG)) {
// 处理正常情况
} else if ((status == TIMEOUT) || (timeout <= 0)) {
// 处理超时
} else {
// 处理错误
}
这种嵌套条件读起来太费劲了。简化条件有几个常用手法:
手法一:提取条件为函数
static bool is_normal_operation(uint8_t status, uint32_t timeout, uint8_t flag) {
return (status == OK) && (timeout > 0) && !(flag & ERROR_FLAG);
}
static bool is_timeout_occurred(uint8_t status, uint32_t timeout) {
return (status == TIMEOUT) || (timeout <= 0);
}
if (is_normal_operation(status, timeout, flag)) {
// 处理正常情况
} else if (is_timeout_occurred(status, timeout)) {
// 处理超时
} else {
// 处理错误
}
手法二:提前返回,减少嵌套
// 重构前
void process_data(Data *data) {
if (data != NULL) {
if (data->valid) {
if (data->size > 0) {
// 处理数据
}
}
}
}
// 重构后
void process_data(Data *data) {
if (data == NULL) return;
if (!data->valid) return;
if (data->size <= 0) return;
// 处理数据
}
提前返回是我最喜欢的手法。它把异常情况提前处理掉,剩下的就是正常逻辑。代码的缩进层次少了,读起来也轻松。
注意:提前返回在函数中间退出时,要确保资源释放正确。比如有 malloc 分配的内存,要在每个 return 前都 free 掉。我建议用 goto 统一处理资源释放,或者用 RAII 风格(如果 C 语言支持)。
四、重构工具:别自己硬扛
手动重构容易出错,尤其是大型项目。我推荐几个好用的工具:
| 工具 | 适用场景 | 我的评价 |
|---|---|---|
| Eclipse CDT | 嵌入式C/C++项目 | 重命名、提取函数很稳,支持交叉引用 |
| Visual Studio + Visual Assist | Windows平台C/C++ | 重构功能强大,但收费 |
| CLion | 跨平台C/C++ | 重构支持好,但需要 CMake 项目 |
| Source Insight | 大型嵌入式项目 | 老牌工具,符号解析强,重构功能一般 |
| sed/awk + grep | 批量文本替换 | 适合简单重命名,但风险高,慎用 |
我个人最常用的是 Eclipse CDT。它有一个「预览」功能,重构前会列出所有受影响的文件,你可以逐个确认。这个功能救过我很多次——有一次重命名一个全局变量,预览发现它被 37 个文件引用,其中 3 个是注释里的,不应该改。如果没有预览,那三个注释里的名字也会被改掉,编译没问题,但文档就乱了。
使用重构工具的小建议:
- 重构前先提交或暂存当前代码,方便回退
- 使用「预览」功能,逐个确认变更
- 重构后跑一遍单元测试和集成测试
- 如果项目没有测试,先加测试再重构
五、重构的节奏:小步快跑
重构最忌讳「大跃进」。我曾经试图在一个周末重构整个通信协议栈,结果周一回来发现编译都过不了。后来我学乖了:每次只改一个点,改完就测试。
推荐的重构节奏:
- 识别坏味道:长函数、重复代码、复杂条件、命名混乱
- 确定重构手法:提取函数、重命名、简化条件、提取常量
- 执行重构:用工具或手动,小步修改
- 验证:编译通过 + 测试通过
- 提交:写清楚重构了什么,为什么重构
你看,重构不是一次性的大工程,而是日常的小习惯。就像打扫房间,每天扫一扫,比年底大扫除轻松多了。
好了,关于代码重构的核心技术就聊到这里。记住:重构不是重写。重写是把代码推倒重来,重构是在现有结构上逐步优化。前者风险大、周期长,后者稳健、可持续。我建议你从今天开始,每次写代码时多问自己一句:「这段代码还能不能再清爽一点?」
公众号:蓝海资料掘金营,微信deep3321