重构手法之函数提炼:提取函数、内联函数、引入解释性变量
各位同学,今天我们来聊聊重构中最基础、也最实用的三个手法。说实话,我在嵌入式这行摸爬滚打十几年,用得最多的就是它们三个。你想想看,窗帘电机这种产品,代码动辄几万行,如果没有这些手法撑着,维护起来简直要命。
提取函数(Extract Method)
什么叫提取函数?说白了,就是把一段代码从原来的函数里「拆」出来,放到一个新函数里。为什么要这么做?因为太长、太复杂的函数,读起来就像一团乱麻。
我举个例子。以前我接手过一个窗帘电机项目,有个函数叫 motor_control(),足足 300 行。里面既有电机速度计算,又有位置检测,还有堵转保护。你想想看,一个人要同时处理这么多事,脑子转得过来吗?
提取函数的核心原则就一条:一个函数只做一件事。怎么做?看下面这个例子:
// 重构前:一个函数干了三件事
void motor_control(void) {
// 计算速度
uint16_t speed = (target_pos - current_pos) * SPEED_FACTOR;
if (speed > MAX_SPEED) speed = MAX_SPEED;
// 检测位置
uint16_t pos_diff = abs(target_pos - current_pos);
if (pos_diff < STOP_THRESHOLD) {
motor_stop();
return;
}
// 堵转保护
static uint16_t last_pos = 0;
if (abs(current_pos - last_pos) < 2) {
stall_counter++;
if (stall_counter > 5) {
motor_stop();
error_flag = 1;
}
} else {
stall_counter = 0;
}
last_pos = current_pos;
motor_set_speed(speed);
}
重构之后,变成这样:
// 重构后:每个函数只做一件事
void motor_control(void) {
uint16_t speed = calc_motor_speed();
check_position();
check_stall_protection();
motor_set_speed(speed);
}
static uint16_t calc_motor_speed(void) {
uint16_t speed = (target_pos - current_pos) * SPEED_FACTOR;
if (speed > MAX_SPEED) speed = MAX_SPEED;
return speed;
}
static void check_position(void) {
uint16_t pos_diff = abs(target_pos - current_pos);
if (pos_diff < STOP_THRESHOLD) {
motor_stop();
}
}
static void check_stall_protection(void) {
static uint16_t last_pos = 0;
if (abs(current_pos - last_pos) < 2) {
stall_counter++;
if (stall_counter > 5) {
motor_stop();
error_flag = 1;
}
} else {
stall_counter = 0;
}
last_pos = current_pos;
}
提取函数的时机:
- 函数超过 30 行,就该考虑拆分了
- 一段代码需要写注释才能看懂,那就把它变成函数名
- 同样的代码逻辑出现两次以上,必须提取
我的经验:提取函数时,命名很重要。函数名要能准确表达「它做了什么」。我习惯用动词开头,比如 calc_xxx()、check_xxx()、update_xxx()。这样读代码的时候,一眼就知道每个函数的作用。
内联函数(Inline Method)
有提取就有内联。内联函数是提取函数的反向操作。什么时候需要内联?当一个函数太简单,简单到它的名字比函数体还长的时候。
我曾经见过这样的代码:
void update_motor_status(void) {
set_motor_running_flag();
}
void set_motor_running_flag(void) {
motor_running = 1;
}
你想想看,update_motor_status() 就调了一个 set_motor_running_flag(),而这个函数就干了一件事——赋值。这种函数留着干嘛?直接内联掉:
void update_motor_status(void) {
motor_running = 1;
}
注意:内联函数不是乱用的。如果这个简单函数将来可能扩展,或者它被多处调用,那就留着。我一般遵循「三次原则」:如果这个函数只在一个地方被调用,而且函数体不超过 3 行,那就内联。
引入解释性变量
这个手法,说白了就是给复杂的表达式起个名字。嵌入式开发里,经常会有一些又长又绕的条件判断,比如:
if ((current_pos > target_pos - HYSTERESIS) &&
(current_pos < target_pos + HYSTERESIS) &&
(motor_status == RUNNING) &&
(!emergency_stop)) {
motor_stop();
}
这段代码,你读一遍能立刻明白它在干嘛吗?反正我不能。引入解释性变量之后:
uint8_t is_in_position = (current_pos > target_pos - HYSTERESIS) &&
(current_pos < target_pos + HYSTERESIS);
uint8_t is_motor_running = (motor_status == RUNNING);
uint8_t is_safe_to_stop = !emergency_stop;
if (is_in_position && is_motor_running && is_safe_to_stop) {
motor_stop();
}
你看,现在读起来是不是清爽多了?每个变量的名字都在告诉你:这个条件代表什么。
引入解释性变量的好处:
- 把复杂的逻辑拆成小块,每个小块都有名字
- 方便调试——你可以单独查看每个变量的值
- 代码自文档化,不需要额外写注释
我个人习惯,在写条件判断时,如果表达式超过 3 个条件,或者嵌套超过 2 层,就一定会引入解释性变量。这招在调试的时候特别管用。我曾经排查过一个 bug,就是靠把条件拆成变量,然后逐个打印,才发现是某个位运算的优先级搞错了。
三个手法的配合使用
这三个手法不是孤立的。在实际项目中,我经常把它们组合起来用。比如:
- 先用提取函数,把大函数拆成小函数
- 再用内联函数,干掉那些多余的中间层
- 最后用解释性变量,让复杂条件变得清晰
你想想看,经过这三步,代码会变成什么样?每个函数都短小精悍,每个变量都意义明确。维护起来,那叫一个舒服。
避坑指南:我曾经在一个项目里,过度提取函数,把代码拆得七零八落。结果一个功能要跳转五六个函数才能看完。后来我学乖了:提取函数时,要考虑调用关系。如果两个函数总是成对出现,那就别拆,或者把它们放在一起。
好了,今天的内容就到这里。这三个手法,看着简单,但真正用好需要大量练习。我建议你从自己手头的代码开始,找一个超过 50 行的函数,试着用今天讲的方法重构一下。相信我,你会爱上这种清爽的感觉。