88、代码重构:如何将一个大函数拆分成小函数?

说实话,我见过太多「千行函数」了。

刚入行那会儿,我也干过这种事——一个函数写完所有逻辑,从初始化到数据处理,再到错误恢复,全塞在一起。结果呢?三个月后自己回来看代码,愣是看了半小时才理清头绪。嗯,这就是典型的「能跑但不好维护」的代码。

今天咱们聊聊,怎么把一个臃肿的大函数,拆成干净利落的小函数。

为什么要拆?大函数有什么问题?

你可能觉得:「能跑就行,拆来拆去多麻烦。」

我跟你讲,这个想法很危险。大函数有几个硬伤:

  • 可读性差——一个函数超过50行,大脑就开始吃力了。超过100行,基本要靠注释才能看懂。
  • 难以测试——大函数里各种分支、循环、嵌套,你很难覆盖所有路径。
  • 复用性低——同样的逻辑散落在不同大函数里,改一处漏三处。
  • 调试困难——出bug了,你得从头到尾捋一遍,定位问题的时间比修bug还长。

核心原则:一个函数只做一件事。如果一件事里还套着另一件事,那就拆。

拆分的具体方法

我习惯用「三步法」来拆大函数。你试试看,很实用。

第一步:识别「逻辑块」

先通读一遍大函数,把代码按功能分成几块。每一块应该是一个相对独立的任务。

举个例子,假设我们有一个处理传感器数据的函数:

// 重构前:一个大函数,做了三件事
void process_sensor_data(void) {
    // 块1:读取原始数据
    uint32_t raw = 0;
    for (int i = 0; i < 8; i++) {
        raw |= (uint32_t)(read_gpio() & 0x01) << i;
        delay_us(10);
    }
    
    // 块2:数据转换
    float voltage = (float)raw * 3.3f / 255.0f;
    float temperature = (voltage - 0.5f) * 100.0f;
    
    // 块3:数据上报
    char buf[32];
    snprintf(buf, sizeof(buf), "TEMP:%.2f", temperature);
    uart_send_string(buf);
}

你看,这里明显有三个逻辑块:读取、转换、上报。每个块都可以独立成一个函数。

第二步:提取成独立函数

把每个逻辑块提取出来,给它们起个好名字。名字要能说明「这个函数做了什么」。

// 重构后:三个小函数,各司其职
static uint32_t read_raw_sensor_data(void) {
    uint32_t raw = 0;
    for (int i = 0; i < 8; i++) {
        raw |= (uint32_t)(read_gpio() & 0x01) << i;
        delay_us(10);
    }
    return raw;
}

static float convert_to_temperature(uint32_t raw) {
    float voltage = (float)raw * 3.3f / 255.0f;
    return (voltage - 0.5f) * 100.0f;
}

static void report_temperature(float temperature) {
    char buf[32];
    snprintf(buf, sizeof(buf), "TEMP:%.2f", temperature);
    uart_send_string(buf);
}

// 主函数变得非常清晰
void process_sensor_data(void) {
    uint32_t raw = read_raw_sensor_data();
    float temperature = convert_to_temperature(raw);
    report_temperature(temperature);
}

小技巧:提取函数时,尽量让每个函数只操作自己的局部变量。全局变量能少用就少用,不然拆了跟没拆一样。

第三步:检查「内聚性」和「耦合度」

拆完之后,你得检查一下:

  • 内聚性——一个函数里的代码,是不是都在做同一件事?如果是,那就对了。
  • 耦合度——函数之间传参多不多?如果参数超过3个,说明拆得还不够细,或者设计有问题。

我在项目中遇到过一种情况:拆出来的函数之间共享了太多全局变量。表面上看是拆了,实际上还是耦合在一起。后来我改成用结构体传参,才真正解耦。

拆分的边界:拆到什么程度算合适?

有人会问:「是不是函数越小越好?」

不是的。拆得太碎,反而会让代码变得零散,阅读时需要在多个函数之间跳来跳去。

我个人习惯遵循几个经验法则:

指标 建议范围 说明
函数行数 10~30行 超过40行,考虑是否还能再拆
参数个数 0~3个 超过4个,考虑用结构体
嵌套层级 不超过3层 超过3层,说明逻辑太复杂
函数职责 1个 能用一句话说清楚它做什么

注意:嵌入式环境里,函数调用有开销。如果拆得太碎,频繁调用小函数可能会影响性能。这时候可以用 static inline 来避免函数调用开销。

一个完整的重构案例

咱们来看一个更复杂的例子。这是一个处理串口命令的函数:

// 重构前:又长又乱
void handle_uart_command(char *cmd) {
    // 解析命令
    char cmd_type = cmd[0];
    int param = atoi(&cmd[2]);
    
    // 执行命令
    if (cmd_type == 'T') {
        // 设置温度阈值
        if (param >= 0 && param <= 100) {
            temp_threshold = param;
            eeprom_write(ADDR_TEMP, param);
            uart_send_string("OK\n");
        } else {
            uart_send_string("ERR:param\n");
        }
    } else if (cmd_type == 'H') {
        // 设置湿度阈值
        if (param >= 0 && param <= 100) {
            hum_threshold = param;
            eeprom_write(ADDR_HUM, param);
            uart_send_string("OK\n");
        } else {
            uart_send_string("ERR:param\n");
        }
    } else {
        uart_send_string("ERR:cmd\n");
    }
}

重构之后:

// 重构后:清晰多了
static bool is_param_valid(int param) {
    return (param >= 0 && param <= 100);
}

static void set_threshold(int *threshold, uint16_t addr, int param) {
    if (!is_param_valid(param)) {
        uart_send_string("ERR:param\n");
        return;
    }
    *threshold = param;
    eeprom_write(addr, param);
    uart_send_string("OK\n");
}

static void handle_temp_command(int param) {
    set_threshold(&temp_threshold, ADDR_TEMP, param);
}

static void handle_hum_command(int param) {
    set_threshold(&hum_threshold, ADDR_HUM, param);
}

void handle_uart_command(char *cmd) {
    char cmd_type = cmd[0];
    int param = atoi(&cmd[2]);
    
    switch (cmd_type) {
        case 'T': handle_temp_command(param); break;
        case 'H': handle_hum_command(param); break;
        default:  uart_send_string("ERR:cmd\n"); break;
    }
}

你看,重构之后:

  • 每个函数只有几行,一眼就能看懂
  • 参数校验被抽出来复用了
  • 主函数变成了一个「调度器」,逻辑非常清晰

拆分的核心流程图

下面这张图总结了大函数拆分的完整流程,你可以照着这个思路来操作:

大函数拆分流程 1. 识别逻辑块 按功能划分代码段落 2. 提取独立函数 给函数起个好名字 3. 检查内聚与耦合 参数≤3,职责单一 是否合格? 重构完成 重新拆分 常见陷阱 • 过度拆分 • 全局变量耦合 • 函数名不清晰

避坑指南

拆函数这件事,说起来简单,做起来容易踩坑。我把自己踩过的坑分享给你:

  • 不要为了拆而拆——如果一个函数只有10行,逻辑也很清晰,那就别拆了。过度拆分反而增加阅读负担。
  • 注意函数命名——我见过有人拆出来的函数叫 func1()func2()……这跟没拆有什么区别?名字要能准确描述功能。
  • 小心全局变量——我曾经把一个函数拆成5个小函数,结果它们都操作同一个全局变量。调试的时候,改一个地方,其他4个函数全受影响。后来我把全局变量改成参数传递,问题才解决。
  • 嵌入式环境要考虑性能——如果拆出来的函数在中断里频繁调用,可以考虑用 inline 或者宏来避免调用开销。

我的习惯:每次写完一个函数,我都会问自己一句话:「这个函数能用一句话说清楚它在做什么吗?」如果说不清楚,那就说明拆得还不够。

好了,关于大函数拆分,今天就聊到这儿。记住一个核心:函数是代码的最小逻辑单元,它应该只做一件事,并且做好这一件事。


公众号:蓝海资料掘金营,微信deep3321