3、坏味道识别(下):发散式变化、霰弹式修改、依恋情结、数据泥团

好,咱们接着聊代码坏味道。上一章我们讲了几个比较直观的坏味道,这一章要聊的四个,说实话,更隐蔽,也更考验你的「嗅觉」。

我个人习惯把这一组坏味道称为「耦合四兄弟」。它们不像长函数那么显眼,但破坏力一点都不小。你想想看,一个系统改着改着就改不动了,往往就是这些坏味道在作祟。

一、发散式变化

什么叫发散式变化?说白了就是:一个模块,因为不同的原因,需要被频繁修改。

举个例子。你有一个工具类,里面既有文件读写,又有字符串处理,还有网络请求。好,现在需求来了:

  • 文件路径变了 → 改这个类
  • 字符串编码改了 → 改这个类
  • 网络协议升级了 → 还是改这个类

你发现问题了吗?这个类承担了太多「变化的原因」。每一次修改,你都得小心翼翼,生怕动了不该动的地方。

核心判断标准:如果一个类有多个「被修改的理由」,那就是发散式变化。

我在项目中遇到过这样一个案例。一个负责数据上报的模块,里面混了日志记录、指标采集、异常处理三件事。每次改日志格式,都要动这个模块;每次加新指标,也要动它;每次调整异常策略,还得动它。后来我实在受不了了,拆成了三个独立的模块。改起来舒服多了。

重构建议:把每个「变化原因」对应的逻辑,抽取到独立的类或模块中。让每个模块只因为一个原因而改变。

二、霰弹式修改

这个坏味道和发散式变化正好相反。发散式变化是「一个地方改多次」,霰弹式修改是「一次改动,要改多个地方」。

你想想看,改一个功能,你得跑遍五六个文件,每个文件改两三行。漏了一个,系统就崩了。这种体验,我相信你也有过。

为什么会这样?因为相关的逻辑被分散到了各个角落。没有集中管理。

坏味道类型 表现 重构方向
发散式变化 一个类因多种原因被改 拆分职责
霰弹式修改 一个改动要改多个类 合并职责

我曾经接手过一个老项目,里面有个「用户权限校验」的逻辑。你猜怎么着?校验代码散落在七个不同的.c文件里。有的在登录模块,有的在菜单模块,有的甚至在数据查询模块。每次改权限规则,我都得拿着搜索工具满项目找。后来我统一抽到了一个 auth_check.c 里,世界清静了。

避坑指南:我曾经以为「把代码分散到各处,可以降低耦合」。后来发现,过度分散反而让修改变得困难。关键是要找到「内聚」和「解耦」的平衡点。

三、依恋情结

这个名字挺有意思的。什么叫依恋情结?就是一个函数,对自己所在的类不感兴趣,反而对另一个类的数据和方法特别「上心」。

看个例子:

// 坏味道:依恋情结
void processOrder(Order *order, Customer *customer) {
    // 这个函数大量使用了 customer 的数据
    if (customer->level == VIP && customer->balance > 1000) {
        order->discount = 0.8;
    } else if (customer->level == NORMAL && customer->balance > 500) {
        order->discount = 0.9;
    }
    // ... 更多对 customer 的操作
}

你看,processOrder 明明属于 Order 类,却一直在跟 Customer 的数据打交道。这说明什么?说明这个函数放错地方了。

我个人习惯的判断方法是:看一个函数里,访问哪个类的成员变量最多。如果大部分时间都在访问另一个类的数据,那就应该搬过去。

重构手法:使用「搬移函数」或「搬移字段」,把依恋的部分挪到它真正归属的类中。

嗯,这里要注意一点:不是所有跨类访问都是坏味道。如果只是偶尔调用一下接口,那很正常。但如果一个函数里,80% 的操作都在操作另一个类的内部数据,那就该警惕了。

四、数据泥团

这个坏味道最好识别。你写代码的时候,有没有发现某些数据项总是「成双成对」地出现?

比如:

  • 地址:省、市、区、街道、邮编
  • 坐标:经度、纬度
  • 时间范围:开始时间、结束时间
  • 金额:原价、折扣价、实付价

这些数据项,如果反复出现在多个函数的参数列表里,或者多个结构体里,那就是「数据泥团」。

// 坏味道:数据泥团
void setAddress(char *province, char *city, char *district, char *street, char *zipcode);
void printAddress(char *province, char *city, char *district, char *street, char *zipcode);
int validateAddress(char *province, char *city, char *district, char *street, char *zipcode);

你看,五个参数,到处传。改一个参数,所有函数都得改。这就是典型的泥团。

重构建议:把这些经常一起出现的数据,封装成一个结构体或类。

// 重构后:封装成结构体
typedef struct {
    char province[32];
    char city[32];
    char district[32];
    char street[128];
    char zipcode[16];
} Address;

void setAddress(Address *addr);
void printAddress(const Address *addr);
int validateAddress(const Address *addr);

封装之后,好处很明显:

  • 参数列表变短了,函数签名更清晰
  • 新增字段时,只需要改结构体定义,不用改所有函数
  • 相关的操作可以集中到结构体的方法中

我记得有一次重构一个支付模块,发现「订单金额」「已付金额」「退款金额」这三个字段,在七八个函数里反复出现。我直接封装了一个 PaymentAmount 结构体,还加了一些校验逻辑。从那以后,金额相关的 bug 几乎绝迹了。

注意:不要为了封装而封装。如果一组数据只在某个函数里出现一次,那没必要硬封装。数据泥团的判断标准是「反复成对出现」。

知识体系总览

下面这张图,帮你快速回顾本章的四个坏味道及其关系:

坏味道识别(下):耦合四兄弟 代码坏味道 发散式变化 霰弹式修改 依恋情结 数据泥团 一个类因多种原因 被频繁修改 → 拆分职责 一次改动要改 多个类/文件 → 合并职责 函数过度依赖 另一个类的数据 → 搬移函数/字段 数据项反复 成对出现 → 封装成类 核心思路:让每个模块「职责单一,内聚性强」

这四个坏味道,说白了都指向同一个问题:职责没有划分清楚。发散式变化是职责太多,霰弹式修改是职责太散,依恋情结是职责放错了地方,数据泥团是职责没有封装。

识别出这些坏味道只是第一步。下一章,我们会聊聊具体的重构手法,告诉你「闻到了臭味,该怎么下手」。嗯,今天就先到这里。

可可精品资料库