第三章:代码坏味道识别——从“能跑”到“好改”的关键一步
说实话,我见过太多“能跑但不敢动”的嵌入式代码了。
尤其是窗帘电机这种产品,代码量不大,但逻辑复杂。你想想看,电机控制、限位检测、遥控接收、堵转保护、掉电记忆……全挤在一个文件里。我接手过一个项目,一个 .c 文件 3000 多行,一个函数 400 行。当时我盯着屏幕,心里就一个念头:这代码,改一个地方,得测三天。
这就是典型的“代码坏味道”。它不是 bug,但比 bug 更可怕。它告诉你:你的代码正在腐烂。
3.1 重复代码——最直观的坏味道
重复代码,说白了就是“复制粘贴”留下的债。我在项目中遇到过好几次:同一个限位判断逻辑,在电机正转、反转、停止三个地方各写了一遍。后来要改限位阈值,好嘛,三个地方都得改,漏一个就出 bug。
典型场景:
- 多个函数中有相同的计算逻辑(如速度曲线计算)
- 不同模块中有相似的初始化代码(如 GPIO、定时器配置)
- 条件分支中重复的代码块(如 if-else 里都有相同的延时处理)
// 坏味道:重复代码
void motor_forward(void) {
// 启动电机
HAL_GPIO_WritePin(MOTOR_EN_GPIO, MOTOR_EN_PIN, GPIO_PIN_SET);
HAL_Delay(10); // 等待电机稳定
// ... 其他逻辑
}
void motor_backward(void) {
// 启动电机
HAL_GPIO_WritePin(MOTOR_EN_GPIO, MOTOR_EN_PIN, GPIO_PIN_SET);
HAL_Delay(10); // 同样的延时,重复了
// ... 其他逻辑
}
// 重构后:提取公共函数
static void motor_enable_with_delay(void) {
HAL_GPIO_WritePin(MOTOR_EN_GPIO, MOTOR_EN_PIN, GPIO_PIN_SET);
HAL_Delay(10);
}
我的习惯:写代码时,如果同一个逻辑我要写第二遍,我就会停下来想想——是不是该抽个函数了?
3.2 过长函数——一个函数做太多事
我见过最夸张的一个函数,叫 motor_control_task,里面包含了:按键扫描、遥控解码、电机启停、限位判断、堵转检测、掉电保存……你猜它有多少行?380 行。
为什么会这样?因为很多人觉得“反正都是电机控制,放一起方便”。但你想过没有,改堵转检测的时候,你得在 380 行里找到那几行代码。万一改错了,可能连按键扫描都受影响。
我曾经踩过的坑:有一次改限位逻辑,不小心把遥控解码的变量给改了。结果电机限位没改好,遥控反而失灵了。排查了整整一个下午。从那以后,我坚持一个函数只做一件事。
判断一个函数是否过长,我有个简单标准:如果这个函数需要你滚动屏幕才能看完,那它就太长了。一般来说,一个函数控制在 20-30 行以内比较合适。
3.3 过大的类——什么都要管的“上帝类”
在嵌入式 C 语言里,虽然没有 class,但我们会用结构体加函数指针模拟面向对象。这时候就容易出现“上帝结构体”——一个结构体包含了电机状态、遥控参数、限位配置、掉电数据、WiFi 连接状态……
我接手过一个项目,一个 Motor_Control_Struct 里塞了 40 多个成员变量。每次加新功能就往里塞,最后这个结构体变得又大又乱。改一个成员,你得担心会不会影响其他 39 个。
重构思路:按职责拆分。比如:
Motor_State_t:只保存电机运行状态Limit_Config_t:只保存限位参数Remote_Data_t:只保存遥控数据
3.4 过长参数列表——函数签名比函数体还长
你见过这样的函数吗?
void motor_set_speed(uint8_t motor_id, uint16_t target_speed,
uint8_t accel_time, uint8_t decel_time,
uint8_t direction, uint8_t enable_brake,
uint16_t brake_delay);
7 个参数!调用的时候你得一个一个数,生怕传错顺序。我见过更夸张的,12 个参数,调用的时候得换行写。
为什么会这样?因为函数承担了太多职责。说白了,它既想设置速度,又想配置加减速,还想控制刹车。一个函数管这么多事,参数自然就多了。
我的建议:如果参数超过 3 个,考虑用结构体传参。或者把函数拆分成多个小函数。比如:
// 重构后:用结构体传参
typedef struct {
uint16_t target_speed;
uint8_t accel_time;
uint8_t decel_time;
uint8_t direction;
} Motor_Speed_Config_t;
void motor_set_speed(uint8_t motor_id, Motor_Speed_Config_t *config);
3.5 发散式变化——一个模块被多个原因修改
发散式变化,说白了就是:一个模块,因为不同的需求,需要反复修改。
举个例子,你的 motor_control.c 里,既包含了电机控制逻辑,又包含了限位配置,还包含了遥控解码。那么当需求变化时:
- 要改电机控制算法 → 改这个文件
- 要改限位阈值 → 改这个文件
- 要改遥控协议 → 还是改这个文件
一个文件被反复修改,改一次测一次,改多了就容易出问题。我在项目中遇到过:改遥控协议的时候,不小心把电机控制的定时器配置给改了,结果电机转起来抖得跟筛子似的。
重构原则:一个模块应该只有一个被修改的理由。把不同职责的代码拆到不同文件里。
3.6 霰弹式修改——改一个功能,要改好多地方
霰弹式修改和发散式变化正好相反。发散式变化是一个地方被多次改,霰弹式修改是一个改动要改很多地方。
我印象很深的一个例子:改电机的堵转电流阈值。好嘛,我得改:
motor_control.c里的堵转检测函数config.h里的宏定义init.c里的初始化代码error_handler.c里的错误处理
改 4 个文件!漏一个就出问题。这就是典型的霰弹式修改。
为什么会这样?因为堵转电流这个“概念”没有集中管理。它分散在多个地方,每个地方都硬编码了一个值或者一个逻辑。
重构的方法很简单:把堵转电流阈值定义成一个全局配置项,所有地方都引用它。这样改一个地方就够了。
3.7 小结——坏味道是重构的信号灯
这六种坏味道,说白了就是代码在向你求救。我个人的经验是:不要等到代码烂到没法改才动手。每次看到坏味道,就顺手重构一下。哪怕只是抽一个函数、拆一个文件,日积月累,代码质量就会慢慢提升。
下一章,我会讲具体的重构手法。但在此之前,我建议你先学会“闻味道”。毕竟,只有知道哪里有问题,才知道怎么改。
一个小练习:打开你手头的窗帘电机代码,数一数有没有超过 100 行的函数?有没有参数超过 5 个的函数?有没有一个结构体里超过 20 个成员?如果有,恭喜你,你已经找到了重构的目标。