3、代码坏味道(下):发散式变化、霰弹式修改、依恋情结、数据泥团
好,咱们接着聊代码坏味道。上一章讲了那些比较「显眼」的问题,这一章要说的几个,其实更隐蔽,也更致命。它们往往不是一眼能看出来的语法错误,而是设计层面的「慢性病」。
我个人的经验是,这些坏味道一旦出现,项目后期维护成本会指数级上升。你想想看,改一个需求,要动十几个文件,或者一个文件里塞了完全不相关的逻辑——这种代码,谁碰谁头疼。
发散式变化(Divergent Change)
什么叫发散式变化?说白了就是:一个模块,因为不同的原因,需要被频繁修改。
举个例子,你有一个 UserService 类,里面既处理用户注册逻辑,又处理发送邮件,还负责生成报表。那么问题来了:
- 邮件模板改了 → 你要改
UserService - 注册规则变了 → 你也要改
UserService - 报表格式调整 → 还是改
UserService
一个类,承担了太多「责任」。每次改动,你都得小心翼翼地在这个大泥球里找对应的代码段。改邮件的时候,不小心碰了注册逻辑,线上就炸了。
我在项目中遇到过这样的案例:一个 OrderService 类,足足有 3000 行。里面混着订单状态机、支付回调、库存扣减、物流查询。每次上线,光是回归测试就要跑半天。后来我们把它拆成了 OrderStateMachine、PaymentHandler、InventoryService、LogisticsService 四个独立的模块。改动范围一下子缩小了 70%。
霰弹式修改(Shotgun Surgery)
这个和发散式变化正好相反。发散式变化是一个类被多个原因改,霰弹式修改是一个原因,要改多个类。
你想想看,改一个需求,要跑遍十几个文件。改完这个改那个,漏掉一个就出 bug。这就是典型的霰弹式修改。
为什么会这样?因为相关的逻辑被分散到了各个角落。比如,你有一个「用户等级」的概念,结果:
- 在
UserController里判断了一次等级 - 在
OrderService里又判断了一次 - 在
DiscountCalculator里又写了一遍 - 在
PrivilegeChecker里还有一份
现在产品说:「用户等级规则要改。」你怎么办?满项目搜索 userLevel,一个一个改。漏一个,线上就出问题。
解决方案其实很简单:把散落的逻辑收拢到一个地方。比如,把用户等级判断封装成一个 UserLevelService,所有地方都调用它。这样改规则时,只改这一个文件就够了。
| 坏味道 | 特征 | 解决方案 |
|---|---|---|
| 发散式变化 | 一个类被多个原因修改 | 拆分职责,单一化 |
| 霰弹式修改 | 一个原因要改多个类 | 合并逻辑,集中管理 |
依恋情结(Feature Envy)
这个坏味道的名字有点文艺,但意思很直白:一个方法,对另一个类的数据「过分依恋」。
说白了,就是方法里频繁地调用另一个类的 getter/setter,或者大量操作另一个类的数据。它自己所在类的数据反而用得很少。
看个例子:
class Order {
calculateDiscount(customer) {
let discount = 0;
if (customer.level === 'VIP') {
discount = customer.totalSpent * 0.2;
} else if (customer.level === 'Normal') {
discount = customer.totalSpent * 0.1;
}
// 这里大量使用了 customer 的数据
// Order 自己的数据反而用得很少
return discount;
}
}
你看,calculateDiscount 方法里,几乎全是在操作 customer 对象的属性。它「依恋」的是 Customer 类,而不是自己所在的 Order 类。
正确的做法是:把这个方法挪到 Customer 类里去。
class Customer {
calculateDiscount() {
let discount = 0;
if (this.level === 'VIP') {
discount = this.totalSpent * 0.2;
} else if (this.level === 'Normal') {
discount = this.totalSpent * 0.1;
}
return discount;
}
}
嗯,这里要注意:依恋情结的本质是「数据和行为应该在一起」。数据在哪里,操作数据的方法就应该在哪里。
this.,又调用了多少次 其他对象.。如果后者远多于前者,那大概率就是依恋情结。
数据泥团(Data Clump)
这个坏味道,我估计每个程序员都见过。就是几个数据项,总是成群结队地出现。
比如:
- 三个参数:
startDate、endDate、format - 四个参数:
city、district、street、zipCode - 两个参数:
width、height
这些数据项,总是在多个方法签名里同时出现。你复制粘贴了无数次,但从来没想过把它们封装成一个对象。
数据泥团的问题在于:
- 参数列表越来越长,可读性差
- 修改时,要改所有出现的地方
- 容易漏掉某个地方的更新
我个人的习惯是:只要一组数据出现超过两次,就考虑封装。
比如上面的地址信息:
// 坏味道:散落的参数
function createUser(city, district, street, zipCode) { ... }
function updateAddress(city, district, street, zipCode) { ... }
function queryByAddress(city, district, street, zipCode) { ... }
// 重构后:封装成 Address 对象
class Address {
constructor(city, district, street, zipCode) {
this.city = city;
this.district = district;
this.street = street;
this.zipCode = zipCode;
}
}
function createUser(address) { ... }
function updateAddress(address) { ... }
function queryByAddress(address) { ... }
封装之后,好处很明显:
- 参数列表变短了
- 如果地址结构变了,只改
Address类就行 - 代码语义更清晰了
好了,这一章的内容就这些。四个坏味道,其实都指向同一个核心问题:代码的组织方式,没有跟上业务逻辑的自然结构。
你想想看,业务上明明是一个整体,代码里却散落各处;业务上明明是不同的概念,代码里却混在一起。这就是坏味道的根源。
下一章,咱们聊聊更高级的坏味道——那些涉及继承和多态的「结构性」问题。到时候见。