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;
}
}
你看,重构之后:
- 每个函数只有几行,一眼就能看懂
- 参数校验被抽出来复用了
- 主函数变成了一个「调度器」,逻辑非常清晰
拆分的核心流程图
下面这张图总结了大函数拆分的完整流程,你可以照着这个思路来操作:
避坑指南
拆函数这件事,说起来简单,做起来容易踩坑。我把自己踩过的坑分享给你:
- 不要为了拆而拆——如果一个函数只有10行,逻辑也很清晰,那就别拆了。过度拆分反而增加阅读负担。
- 注意函数命名——我见过有人拆出来的函数叫
func1()、func2()……这跟没拆有什么区别?名字要能准确描述功能。 - 小心全局变量——我曾经把一个函数拆成5个小函数,结果它们都操作同一个全局变量。调试的时候,改一个地方,其他4个函数全受影响。后来我把全局变量改成参数传递,问题才解决。
- 嵌入式环境要考虑性能——如果拆出来的函数在中断里频繁调用,可以考虑用
inline或者宏来避免调用开销。
我的习惯:每次写完一个函数,我都会问自己一句话:「这个函数能用一句话说清楚它在做什么吗?」如果说不清楚,那就说明拆得还不够。
好了,关于大函数拆分,今天就聊到这儿。记住一个核心:函数是代码的最小逻辑单元,它应该只做一件事,并且做好这一件事。
公众号:蓝海资料掘金营,微信deep3321