2. 代码坏味道(上):重复代码、过长函数、过大的类、过长参数列表

大家好,我是你们的老朋友。今天咱们聊聊代码里的「坏味道」。

别误会,不是代码真的馊了。而是有些写法,一看就知道——这里迟早要出事。我做了十几年C语言开发,踩过的坑比写过的代码还多。今天这四种坏味道,几乎每个项目里都能碰到。

核心观点:坏味道不是bug,但它是bug的温床。闻到味道就改,别等它发酵。

代码坏味道(上)知识体系 重复代码 过长函数 过大的类 过长参数列表 维护成本翻倍 难以理解和测试 职责混乱 调用方脆弱 提取函数/宏 拆分函数 拆分模块 封装结构体 识别 → 理解后果 → 采取重构策略

2.1 重复代码:最懒的写法,最痛的代价

重复代码是什么?说白了,就是同一段逻辑在多个地方出现。你复制粘贴,我复制粘贴,大家都这么干。

我见过一个项目,同样的校验逻辑在五个文件里各写了一遍。后来需求改了,改代码的人只改了其中三个文件。结果呢?线上出了事故,查了两天才发现是漏改了。

我曾经在一个嵌入式项目里,因为重复的延时函数没统一,导致不同模块的时序偏差。那一次,整个产线停了两天。从那以后,我对重复代码零容忍。

怎么识别?很简单:

  • 看到两段代码长得像双胞胎
  • 一个函数里出现多次相同的逻辑块
  • 不同文件里有几乎一样的结构体定义

怎么改?提取成函数或宏。举个例子:

// 坏味道:重复代码
void processA() {
    int i;
    for (i = 0; i < 100; i++) {
        buffer[i] = buffer[i] * 2 + 1;
    }
}

void processB() {
    int i;
    for (i = 0; i < 100; i++) {
        buffer[i] = buffer[i] * 2 + 1;
    }
}

// 重构后
static inline void transform_buffer(int *buf, int len) {
    for (int i = 0; i < len; i++) {
        buf[i] = buf[i] * 2 + 1;
    }
}

void processA() { transform_buffer(buffer, 100); }
void processB() { transform_buffer(buffer, 100); }

我个人习惯:只要同一段代码出现第三次,就必须提取。第一次可能是巧合,第二次是偷懒,第三次就是技术债了。

2.2 过长函数:一个函数干十件事

你见过一个函数超过200行吗?我见过。而且不止一个。

过长函数的问题在于——你根本不知道它到底在干什么。读代码的人要上下翻好几屏,才能勉强理清逻辑。更可怕的是,这种函数往往耦合了多个职责,改一处可能崩十处。

为什么会这样?说白了,就是「顺手」二字。写代码时觉得「再加一个功能也没啥」,结果越加越多,最后成了一团乱麻。

我建议:一个函数最好控制在30行以内。超过50行,就该考虑拆分了。

// 坏味道:过长函数
void handle_data(int *data, int len, int mode) {
    // 校验
    if (data == NULL || len <= 0) return;
    // 过滤
    for (int i = 0; i < len; i++) {
        if (data[i] < 0) data[i] = 0;
    }
    // 转换
    if (mode == 1) {
        for (int i = 0; i < len; i++) {
            data[i] = data[i] * 2;
        }
    } else if (mode == 2) {
        for (int i = 0; i < len; i++) {
            data[i] = data[i] / 2;
        }
    }
    // 输出
    for (int i = 0; i < len; i++) {
        printf("%d ", data[i]);
    }
}

// 重构后
static int is_valid(int *data, int len) {
    return data != NULL && len > 0;
}

static void clamp_data(int *data, int len) {
    for (int i = 0; i < len; i++) {
        if (data[i] < 0) data[i] = 0;
    }
}

static void transform_data(int *data, int len, int mode) {
    for (int i = 0; i < len; i++) {
        data[i] = (mode == 1) ? data[i] * 2 : data[i] / 2;
    }
}

static void print_data(int *data, int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", data[i]);
    }
}

void handle_data(int *data, int len, int mode) {
    if (!is_valid(data, len)) return;
    clamp_data(data, len);
    transform_data(data, len, mode);
    print_data(data, len);
}

关键点:每个函数只做一件事。做好一件事。只做一件事。

2.3 过大的类:一个结构体装下全世界

C语言里没有类,但结构体就是我们的「类」。过大的结构体,什么字段都往里塞,最后成了一个「万能口袋」。

我在项目中遇到过这样的结构体:

// 坏味道:过大的结构体
struct device {
    int id;
    char name[64];
    int type;
    // 网络相关
    char ip[16];
    int port;
    // 传感器相关
    float temperature;
    float humidity;
    // 配置相关
    int baudrate;
    int parity;
    // 状态相关
    int status;
    int error_code;
    // 日志相关
    char log_path[256];
    int log_level;
    // ... 还有20多个字段
};

你想想看,这个结构体到底代表什么?它既管网络,又管传感器,还管日志。改网络的人要小心别碰了传感器字段,改日志的人又怕影响状态。耦合度极高。

我曾经接手过一个项目,一个结构体有80多个字段。每次加新功能都要在这个结构体里加字段,最后编译都慢得不行。重构时拆成了6个独立的结构体,代码瞬间清爽了。

怎么拆?按职责分:

// 重构后
struct network_config {
    char ip[16];
    int port;
};

struct sensor_data {
    float temperature;
    float humidity;
};

struct serial_config {
    int baudrate;
    int parity;
};

struct device_status {
    int status;
    int error_code;
};

struct device {
    int id;
    char name[64];
    int type;
    struct network_config net;
    struct sensor_data sensor;
    struct serial_config serial;
    struct device_status status;
};

我个人习惯:一个结构体超过10个字段,我就会停下来想想——是不是该拆了?

2.4 过长参数列表:函数签名比函数体还长

你见过这样的函数吗?

// 坏味道:过长参数列表
int configure_device(int id, char *name, char *ip, int port, 
                     int baudrate, int parity, int mode, 
                     int timeout, int retry_count, int log_level);

调用这个函数的人,得记住10个参数的顺序和含义。少传一个?编译报错。传错顺序?运行时崩溃。这种函数,说白了就是在折磨调用者。

为什么会这样?因为写函数的人图省事,把所有配置项都塞进参数列表里。他觉得「这样调用方灵活」,实际上调用方只想骂人。

怎么改?把相关参数封装成结构体:

// 重构后
struct device_config {
    int id;
    char *name;
    char *ip;
    int port;
    int baudrate;
    int parity;
    int mode;
    int timeout;
    int retry_count;
    int log_level;
};

int configure_device(struct device_config *cfg);

这样调用方只需要填充结构体,传一个指针就行。而且以后加参数,不用改函数签名,结构体里加字段即可。

原则:参数超过3个,就该考虑封装。超过5个,必须封装。

小结

这四种坏味道,说白了都是「偷懒」的代价。重复代码是懒得提取,过长函数是懒得拆分,过大的类是懒得解耦,过长参数列表是懒得封装。

但你要知道——今天偷的懒,明天都要加倍还。我见过太多项目,因为早期不重视这些坏味道,后期重构成本高到离谱,最后只能推倒重来。

嗯,今天就聊到这儿。记住:闻到坏味道就改,别等它发酵。


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