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

好,咱们接着聊坏味道。上一章讲了几个常见的,这一章我挑了几个更“隐蔽”的。说实话,这些坏味道在项目里特别容易“抱团出现”。你发现一个,往往能顺藤摸瓜揪出一串。

我个人习惯是,先看代码里哪些地方改起来“特别费劲”。如果改一个功能要动七八个文件,那八成是霰弹式修改或发散式变化在作祟。如果改一个字段导致十几个方法都得跟着变,那多半是可变数据没管好。

咱们一个一个来拆解。

可变数据(Mutable Data)

说白了,就是数据太“自由”了。谁都能改,改完还没人通知你。我见过最夸张的一个项目,一个全局的订单状态字段,被六个不同的服务直接赋值。线上出问题查了半天,最后发现是某个定时任务偷偷把状态改了。

为什么会这样?因为可变数据让代码的“副作用”变得不可控。你调用一个方法,它可能悄悄改了某个全局变量,然后另一个方法就“莫名其妙”地挂了。

典型症状:

  • 全局变量满天飞
  • 同一个对象在不同方法里被反复修改
  • 返回值是 void,但方法内部改了传入的参数

怎么治?我建议遵循“最小可变原则”。能用局部变量就别用全局的,能用 final 就加上 final。如果确实需要共享状态,考虑用“不可变对象”或者“事件溯源”的方式。

我的经验:重构时,先把所有 public 字段改成 private,然后加 getter。如果发现某个字段只在构造时赋值一次,直接上 final。这一步做完,代码的“安全感”会提升不少。

发散式变化(Divergent Change)

这个坏味道的名字有点绕,但意思很简单:一个类,因为不同的原因,需要被频繁修改

举个例子,你有一个 OrderService 类。今天因为支付逻辑变了,你要改它;明天因为库存规则变了,你还要改它;后天因为通知模板变了,你仍然要改它。你想想看,这个类是不是成了“万能垃圾桶”?

这就是发散式变化。一个类承担了太多“职责”,导致任何风吹草动都得动它。

避坑指南:我曾经接手过一个项目,一个 3000 行的 ReportGenerator 类,里面混着 SQL 拼接、HTML 渲染、PDF 生成、邮件发送。每次改报表格式,都要在这个类里翻半天。后来我花了三天,把它拆成了五个独立的类。改起来舒服多了。

重构手法很简单:提取类(Extract Class)。把每个“变化原因”对应的代码,单独抽成一个类。这样,支付变了只改支付类,库存变了只改库存类。

霰弹式修改(Shotgun Surgery)

这个和发散式变化正好相反。发散式变化是一个类被改很多次,霰弹式修改是改一个功能,要动很多个类

比如,你要给系统加一个“会员等级”字段。结果发现,需要在 User 类加字段、在 OrderService 里加判断、在 DiscountCalculator 里加逻辑、在 EmailTemplate 里加变量、在 ReportGenerator 里加统计……改完一圈,你都不知道有没有漏掉哪个地方。

这就是典型的霰弹式修改。一个逻辑散落在各个角落,改起来像打地鼠。

坏味道 核心问题 重构手法
发散式变化 一个类被多个原因修改 提取类
霰弹式修改 一个修改需要动多个类 搬移函数/字段

怎么治?搬移函数(Move Function)搬移字段(Move Field)。把散落的逻辑集中到一个地方。比如上面那个会员等级的例子,就应该把所有相关逻辑都收拢到 MembershipService 里。

我的习惯:每次改代码前,先问自己一句:“这个改动,我需要改几个文件?”如果超过三个,我就停下来想想,是不是该先重构再改功能。

依恋情结(Feature Envy)

这个坏味道特别有意思。它说的是:一个方法,对另一个类的数据“过分感兴趣”

比如,你有一个 Order 类,里面有个 calculateDiscount 方法。结果这个方法里,全是 customer.GetLevel()customer.GetTotalSpent()customer.GetRegion()……你想想看,这个方法是不是更应该在 Customer 类里?

说白了,就是方法“嫉妒”别的类的数据,老想跑到别人家里去干活。

判断标准:看一个方法里,调用 this 的次数多,还是调用其他对象的 getter 多。如果后者明显更多,那就是依恋情结。

重构手法:搬移函数(Move Function)。把这个方法挪到它真正“依恋”的那个类里去。如果挪过去之后,发现原类还需要这个方法,那就留一个委托方法过渡一下。

避坑指南:我曾经犯过一个错,看到一个方法里调了太多 order.GetItems(),就直接把方法搬到了 Order 类。结果发现这个方法还依赖了其他几个类的数据,搬过去反而让 Order 类变臃肿了。嗯,这里要注意:搬移之前,先看看这个方法到底“依恋”的是哪个类。如果它同时依恋多个类,那可能需要先做参数对象(Introduce Parameter Object)。

数据泥团(Data Clump)

这个坏味道最“直观”。你经常看到三四个字段总是“成双成对”地出现。比如 startDateendDatecitystreetzipCodewidthheight

如果这些字段总是在一起出现,那它们就是一个“数据泥团”。

为什么这是个坏味道?因为每次你都要重复声明它们,传参时要传一堆,改起来也要改一堆。而且,它们往往暗示着“这里应该有一个对象”。

典型场景:

  • 方法参数列表里,连续三个以上参数来自同一个“组”
  • 多个类里都有相同的字段组合
  • 数据库表里,总有几列是一起查询的

重构手法:提炼类(Extract Class)引入参数对象(Introduce Parameter Object)。把那些总是一起出现的字段,封装成一个新的类。

比如,把 startDateendDate 封装成 DateRange 类。然后你会发现,很多关于日期范围的逻辑(比如判断是否重叠、是否包含某个日期)都可以放到这个新类里。代码瞬间就干净了。

我的经验:数据泥团是“新手友好”的坏味道。因为它太好识别了。你只要看到三个以上的字段总是同时出现,就可以考虑封装。而且,封装之后往往能发现新的行为。比如我之前把 widthheight 封装成 Dimension 类后,顺手就加了个 Area() 方法和 IsSquare() 方法。嗯,这就是重构带来的“意外收获”。

知识体系总览

下面这张图,我把这五个坏味道的关系和重构手法梳理了一下。你可以把它当作一个“快速诊断卡”。

坏味道识别(下)知识体系 可变数据 发散式变化 霰弹式修改 依恋情结 数据泥团 对应重构手法 封装变量 / 拆分变量 提取类 搬移函数 / 搬移字段 搬移函数 提炼类 / 引入参数对象 箭头指向:坏味道 → 推荐的重构手法

这五个坏味道,说白了都指向同一个问题:代码的“职责”没有分清楚。数据该归谁管、逻辑该放哪里、修改该影响哪些地方——这些边界一旦模糊,坏味道就来了。

我个人习惯是,每次重构完一个类,都会问自己一句:“如果明天要改这里,我需要改几个地方?”如果答案是“一个”,那基本就对了。如果答案是“好几个”,那说明还有坏味道没清干净。

嗯,这一章的内容就到这儿。记住,识别坏味道只是第一步,真正有价值的是动手去改。下一章咱们会聊具体的重构手法,到时候拿真实代码来练手。


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