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 几乎绝迹了。
注意:不要为了封装而封装。如果一组数据只在某个函数里出现一次,那没必要硬封装。数据泥团的判断标准是「反复成对出现」。
知识体系总览
下面这张图,帮你快速回顾本章的四个坏味道及其关系:
这四个坏味道,说白了都指向同一个问题:职责没有划分清楚。发散式变化是职责太多,霰弹式修改是职责太散,依恋情结是职责放错了地方,数据泥团是职责没有封装。
识别出这些坏味道只是第一步。下一章,我们会聊聊具体的重构手法,告诉你「闻到了臭味,该怎么下手」。嗯,今天就先到这里。