首页/开发/代码质量与评审速查/坏味道:霰弹式修改(Shotgun Surgery)
重构手法62 条

坏味道:霰弹式修改(Shotgun Surgery)

一个变化需要同时改很多处(分散在各处的相似修改)时,用 Move Function、Move Field、Inline Class 把相关代码聚拢;目标是单一修改点。

什么是霰弹式修改

霰弹式修改指一个变化需要同时改动很多处:改一个字段要在十几个文件里做相似的修改,漏掉一处就在某条路径上出错。它与发散式变化互为镜像——那里是一个模块因多种原因被修改,这里是同一个原因要改很多模块。处方是把散落的相关代码与数据聚拢到一个模块,让一个概念只留一个修改点。

基本信息

典型信号改一个字段或规则要动十几个文件
处方移动函数、移动字段、合并到同一类、内联多余类
反向对照发散式变化:一个类因多种原因被反复修改
判断问题改一处容易漏几处
收益单一修改点,回归范围可预期

典型使用场景

  • 新增一种支付方式,发现要在校验、计费、对账、通知四处加分支
  • 字段改名需要同步多处数据结构与转换代码
  • 税率或有效期规则散落在多个类中,改一次要全体比对
  • 同一份常量在多个模块各写一遍,取值不一致时难以发现
  • 上线清单要靠人记还有哪些地方要改

聚拢思路

聚拢思路
// 分散:多处各自计算有效期,规则改一次要全部同步
boolean valid(Subscription s) { return now.isBefore(s.getEnd()); }

// 聚拢:一个模块负责有效期这个概念
class Validity {
    boolean isValid(Instant now) { ... }       // 判断逻辑集中在此
    boolean isExpiringSoon(Instant now) { ... }
}
// 调用方只依赖 Validity,规则变化只改这一个类

现象与处理

现象说明处理
一处改动引发多处相似修改变化没有收拢到单一模块把相关代码与数据移到同一模块
修改后总有漏掉的点缺少统一入口抽出统一入口,让调用方依赖它
相似逻辑出现在多个文件概念被复制先合并实现,再考虑参数化差异
动手前先做完整盘点:把所有需要同步修改的位置列出来,逐处对照行为是否一致。集中过程中最容易在边角条件上出现静默的行为改变。
集中时不要顺手改行为:先做纯搬迁(结构等价),把行为调整放进单独提交;出问题时才能判断是搬迁引入的还是改动引入的。

易错点

  • 集中散落逻辑时顺带改行为:集中前后没有逐一对照,缺陷随重构一起进入主干
  • 只把代码搬到新类而不接入调用方:新旧两份逻辑并存,比原来更混乱
  • 按文件而不是按概念聚拢:拆出一堆工具类,变化原因依然分散
  • 忽略镜像问题:把不相关的东西堆进同一模块,等于换一种方式耦合

数据来源与参考

  • 《重构》第 2 版 · 第 3 章(Shotgun Surgery / Divergent Change)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌