返回博客

DRY 害了我——用「消除重复」把历史遗留假设写死进常量的踩坑

某次例行代码整理,我扫到三个不同的 service 文件里都有这样一段逻辑:

if (['PT01', 'PT02', 'PT03', 'SR01', 'SR02'].includes(code)) { ... }

同样的字面量数组,出现了三次。职业本能告诉我:消掉重复,抽成常量。于是我提了一个 PR,新增了:

export const PATENT_SOFTWARE_LEADER_CODES = ['PT01', 'PT02', 'PT03', 'SR01', 'SR02']

三处引用全部替换,代码整洁了,PR 通过。

但没多久,一个业务侧的问题让整件事崩了:「为什么这里要把专利类和软著类放在一起?」

现象

这个常量名 PATENT_SOFTWARE_LEADER_CODES 听起来像一个既定的业务概念。任何读到它的人都会默认——「哦,这两类组长权限是被系统定义捆绑在一起的」。但翻历史记录才发现:

  • 三年前这两个业务线曾经合并,共用同一个负责人,所以权限判断写在了一起
  • 一年前业务线拆分了,但有一个接口没有跟上,保留了旧的判断列表
  • 这三处调用各自的语义已经发生了偏移——有的是审批流历史订单判断,有的是当前权限校验,有的是报表导出过滤

我的「消除重复」,把一个已经失效的历史合并关系固化成了一个看起来有权威感的命名常量。后续维护者如果只看这个常量,会以为「PT01-03 和 SR01-02 永远是一个整体」,从而在未来的需求里做出错误的决策。

根因

技术重复和业务分组是两个完全不同的信号,但它们长得一模一样:

| 信号 | 含义 | 是否需要验证 | |------|------|------------| | 技术重复:同一字面量出现 N 次 | 代码可以 DRY | 不需要业务验证 | | 业务分组:这 N 个值属于同一概念 | 编码的是业务规则 | 必须和业务对齐 |

DRY 原则处理的是第一个信号——语法层面的冗余。但我的抽取操作偷偷声明了第二件事:「这些值在业务上是同一组」。这个声明我从没验证过,它只是从代码表象里「推断」出来的。

坑:常量名本身就是一条业务断言

一旦常量有了名字,读者就会把它当作「系统的设计决策」来信任。如果这个分组实际上是历史合组的残留、团队拆分后的遗忘、或者三个独立调用场景的意外巧合——那你的常量就在用权威的外表传播错误假设,且越久越难撤。

正解

在提取任何多值分组之前,先问三个问题:

Q1:这个分组今天还有业务名字吗? 能让一个产品或业务同学一口气说出来的,才算真实概念。需要用「+」或「和 X 与 Y」拼出来的——你是在发明概念,不是在识别它。

Q2:这个分组有单一的决策者吗? 「当年合并的时候就放一起了,后来……」这种回答说明它是历史产物,不要提取。

Q3:如果明天要改这个分组,改常量能准确反映业务变化吗? 如果三个调用方语义已经分化,改一个常量会同时错误地影响另外两处——此时保留重复才是诚实的,因为它们需要被独立修改。

正确的处理方式不是「提取」,而是加注释做标记

// 2022 年专利组和软著组合并时共用同一套组长判断;
// 2023 年已拆分。此处保留是因为审批流历史订单仍依赖旧规则。
// 如需修改请确认不影响其他两处:reports-service.ts:88, permission.ts:34
if (['PT01', 'PT02', 'PT03', 'SR01', 'SR02'].includes(code)) { ... }

这比一个响亮的常量名更诚实,也更容易让接手的人做出正确判断。

如果确认分组当前有效,才提取——但名字要来自业务语言,不是来自技术联合:

// GOOD:名字来自业务,有单一 owner,能在配置表里追溯
export const VIP_USER_LEVELS = [5, 6, 7]
 
// BAD:复合名 = 两个概念的技术拼接,不是一个业务概念
export const HR_AND_FINANCE_APPROVERS = [...]
正解:DRY 消除语法冗余,但不授权声明业务分组

看到 N 处相同字面量,先 grep 出所有调用方,逐一写出「这里为什么用这个值」。如果三句话解释不能对上,就不要提取——留着重复,加上面包屑注释,让下一个人能独立判断。

一句话外卖

重复的字面量只是「巧合」,常量名会把它升格为「承诺」——在提取之前,先问清楚这个承诺到底是谁签的、还算不算数。