重构手法之函数提炼:提取函数、内联函数、引入解释性变量

各位同学,今天我们来聊聊重构中最基础、也最实用的三个手法。说实话,我在嵌入式这行摸爬滚打十几年,用得最多的就是它们三个。你想想看,窗帘电机这种产品,代码动辄几万行,如果没有这些手法撑着,维护起来简直要命。

提取函数(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,就是靠把条件拆成变量,然后逐个打印,才发现是某个位运算的优先级搞错了。

三个手法的配合使用

这三个手法不是孤立的。在实际项目中,我经常把它们组合起来用。比如:

  1. 先用提取函数,把大函数拆成小函数
  2. 再用内联函数,干掉那些多余的中间层
  3. 最后用解释性变量,让复杂条件变得清晰

你想想看,经过这三步,代码会变成什么样?每个函数都短小精悍,每个变量都意义明确。维护起来,那叫一个舒服。

避坑指南:我曾经在一个项目里,过度提取函数,把代码拆得七零八落。结果一个功能要跳转五六个函数才能看完。后来我学乖了:提取函数时,要考虑调用关系。如果两个函数总是成对出现,那就别拆,或者把它们放在一起。

好了,今天的内容就到这里。这三个手法,看着简单,但真正用好需要大量练习。我建议你从自己手头的代码开始,找一个超过 50 行的函数,试着用今天讲的方法重构一下。相信我,你会爱上这种清爽的感觉。