识别代码坏味道:从“能跑”到“好改”的第一步

说实话,我见过太多“能跑但不敢动”的C代码了。

有一次我接手一个嵌入式项目,光是一个函数就写了800多行。我当时盯着屏幕,心里就一个念头:这代码,改一行崩一片。后来我花了整整两周,才把那个函数拆成十几个小函数。

你想想看,如果代码本身就在“坏掉”的边缘,你加功能、修Bug,只会让它越来越烂。所以,识别代码坏味道,是重构的第一步,也是最关键的一步。

核心观点:坏味道不是Bug,但它预示着Bug即将到来。识别它,比修复它更重要。

1. 过长函数:一个函数只做一件事

我个人的习惯是:一个函数超过30行,我就开始警惕。超过50行,基本就要拆了。

为什么?因为函数越长,你越难理解它在干什么。你想想看,一个函数里既有初始化、又有数据处理、还有错误恢复,你读代码的时候脑子得来回切换上下文,累不累?

// 坏味道:过长函数
void process_data(int *data, int len) {
    // 初始化
    int sum = 0;
    for (int i = 0; i < len; i++) {
        sum += data[i];
    }
    // 计算平均值
    float avg = (float)sum / len;
    // 打印结果
    printf("Sum: %d, Avg: %.2f\n", sum, avg);
    // 写入文件
    FILE *fp = fopen("result.txt", "w");
    if (fp) {
        fprintf(fp, "%d %.2f\n", sum, avg);
        fclose(fp);
    }
    // 错误处理
    if (avg < 0) {
        printf("Error: negative average\n");
    }
}

你看,这个函数干了五件事。我建议拆成五个小函数,每个只做一件事。

我的经验:拆函数的时候,别怕函数名长。比如 calculate_average()write_result_to_file(),名字长一点没关系,关键是看一眼就知道它干什么。

2. 过大的类:一个类管太多事

在C语言里,虽然没有“类”这个关键字,但我们用结构体+函数指针模拟面向对象时,很容易造出一个“巨无霸”结构体。

我记得有一次,一个结构体里塞了30多个字段,有配置参数、有状态标志、有缓冲区、还有回调函数指针。我当时就一个感觉:这结构体,谁改谁出事。

// 坏味道:过大的结构体
typedef struct {
    // 配置参数
    int baud_rate;
    int data_bits;
    int stop_bits;
    char parity;
    // 状态标志
    int is_connected;
    int is_sending;
    int is_receiving;
    // 缓冲区
    char tx_buffer[1024];
    char rx_buffer[1024];
    int tx_len;
    int rx_len;
    // 回调函数
    void (*on_send_done)(void);
    void (*on_recv_done)(void);
    void (*on_error)(int err_code);
    // 统计信息
    int total_sent;
    int total_recv;
    int error_count;
} UartManager;

这个结构体,说白了就是“串口管理器”什么都管。我建议拆成:UartConfigUartStatusUartBufferUartCallbacksUartStats。每个结构体只负责一件事。

注意:拆结构体的时候,别拆得太碎。我见过有人把一个结构体拆成十几个小结构体,结果代码反而更难读了。适度拆分,才是关键。

3. 重复代码:复制粘贴一时爽,维护火葬场

重复代码,是我在项目中遇到最多的坏味道。没有之一。

我曾经在一个项目里,发现同一个校验函数被复制粘贴了5次,每次只改了一个参数。我当时就想:写这段代码的人,是不是复制粘贴上瘾了?

// 坏味道:重复代码
int validate_uart_config(UartConfig *cfg) {
    if (cfg->baud_rate < 9600 || cfg->baud_rate > 115200) return -1;
    if (cfg->data_bits < 5 || cfg->data_bits > 8) return -1;
    if (cfg->stop_bits < 1 || cfg->stop_bits > 2) return -1;
    return 0;
}

int validate_spi_config(SpiConfig *cfg) {
    if (cfg->clock_speed < 100000 || cfg->clock_speed > 10000000) return -1;
    if (cfg->data_bits < 8 || cfg->data_bits > 16) return -1;
    if (cfg->mode < 0 || cfg->mode > 3) return -1;
    return 0;
}

你看,这两个函数结构一模一样,只是校验的参数不同。我建议提取一个通用的校验框架:

// 重构后:消除重复
typedef struct {
    const char *name;
    int min;
    int max;
} RangeCheck;

int validate_range(RangeCheck *checks, int count, int *values) {
    for (int i = 0; i < count; i++) {
        if (values[i] < checks[i].min || values[i] > checks[i].max) {
            printf("%s out of range\n", checks[i].name);
            return -1;
        }
    }
    return 0;
}

避坑指南:我曾经以为“就复制一次,没事的”。结果三个月后,需求变了,我改了5个地方,漏了2个。从那以后,我再也不复制粘贴了。

4. 过长参数列表:参数超过3个,就该考虑结构体了

我个人习惯:函数参数超过3个,我就开始考虑用结构体传参。超过5个,那基本就是坏味道了。

为什么?因为参数越多,调用者越容易搞错顺序。你想想看,一个函数有7个参数,你调用的时候得一个一个对,烦不烦?

// 坏味道:过长参数列表
void send_packet(int fd, char *data, int len, int timeout, int retry_count, int use_checksum, int priority) {
    // ...
}

// 重构后:使用结构体
typedef struct {
    int fd;
    char *data;
    int len;
    int timeout_ms;
    int retry_count;
    int use_checksum;
    int priority;
} SendPacketParams;

void send_packet(SendPacketParams *params) {
    // ...
}

你看,用结构体传参,调用的时候只需要填字段,顺序错了也没关系。而且以后加参数,不用改函数签名,多方便。

注意:别把所有参数都塞进一个结构体。如果有些参数是“配置类”的,有些是“运行时”的,建议分开。我见过有人把配置和运行时参数混在一起,结果代码越改越乱。

5. 全局变量滥用:全局变量是“隐式依赖”的元凶

全局变量,是C语言里最容易被滥用的特性。没有之一。

我记得有一次,一个项目里用了30多个全局变量。我改一个函数,结果其他三个函数的行为都变了。我当时就一个感觉:这代码,改一行崩一片。

// 坏味道:全局变量滥用
int g_baud_rate;
int g_data_bits;
int g_stop_bits;
char g_parity;
int g_is_connected;
int g_error_count;

void init_uart() {
    g_baud_rate = 115200;
    g_data_bits = 8;
    g_stop_bits = 1;
    g_parity = 'N';
    g_is_connected = 0;
    g_error_count = 0;
}

void send_data(char *data, int len) {
    if (!g_is_connected) {
        printf("Not connected\n");
        return;
    }
    // 使用 g_baud_rate, g_data_bits 等
}

你看,send_data() 依赖了 g_is_connected,但你在别的地方改了它,send_data() 的行为就变了。这就是“隐式依赖”。

我建议:把全局变量封装到结构体里,通过函数访问:

// 重构后:封装全局变量
typedef struct {
    int baud_rate;
    int data_bits;
    int stop_bits;
    char parity;
    int is_connected;
    int error_count;
} UartContext;

static UartContext ctx;  // 只在当前文件可见

void uart_init() {
    ctx.baud_rate = 115200;
    ctx.data_bits = 8;
    ctx.stop_bits = 1;
    ctx.parity = 'N';
    ctx.is_connected = 0;
    ctx.error_count = 0;
}

int uart_is_connected() {
    return ctx.is_connected;
}

我的经验:全局变量不是不能用,但一定要控制可见范围。能用 static 就用 static,能封装就封装。我见过最惨的一次,一个全局变量被20个函数读写,最后谁都不敢改。

知识体系总览

下面这张图,是我总结的“代码坏味道识别”的核心逻辑。你可以把它当作一个检查清单:

代码坏味道识别 · 知识体系 代码坏味道 过长函数 过大的类/结构体 重复代码 过长参数列表 全局变量滥用 解决方案: 拆函数 · 拆结构体 · 提取公共逻辑 · 用结构体传参 · 封装全局变量

总结一下

识别代码坏味道,说白了就是培养一种“代码嗅觉”。你看到一段代码,心里要有个判断:这代码,以后改起来会不会痛?

我个人习惯是:每次提交代码前,都过一遍这五个检查点。如果发现坏味道,就顺手重构。别等到代码烂到没法改的时候再动手,那时候就晚了。

一句话记住:好代码不是写出来的,是改出来的。识别坏味道,就是改对方向的第一步。

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