2560 字
约 8 分钟
15
S22-A16 · 命名的漂移:为什么代码里出现三种叫法

同一份代码里,"用户"有时叫 user、有时叫 member、有时叫 account;"状态"有时叫 status、有时叫 state、有时叫 phase。每个名字在当时都合理,因为它们各自出现在不同的模块里——而当它们开始互相调用时,读者必须在三套词汇之间做翻译。

命名漂移很少是"取名字不认真",而是缺少一个"词汇表"作为单一来源。更棘手的是:漂移是渐进的,没有人做过"错误的决定"。每一步都合理,合起来才不可读——所以它很难在评审里被拦,因为没有一处可以指责。
而这个词表在项目初期往往不存在,等到需要它时,已经有三套名字了。


1. 漂移的四种成因

成因 表现
多人独立命名 同概念不同词
翻译差异 member/account/user
领域演变 旧名沿用,语义已变
外部系统影响 数据库用词与代码不一致

第三项最隐蔽:一个字段叫 type 而它的含义已经从"类型"变成"权限级别",但名字没改——因为改名要动的地方很多。

第四项在跨系统时常见:数据库列名 user_flag、接口字段 status、代码字段 state,三处指的是同一件事,而新人必须查三次才知道。第一项(多人独立命名)在团队扩张时集中爆发:五个新人各写一个模块,五套词汇就出现了,而他们各自都觉得自己用的是行业惯例。

2. 漂移的代价在哪

代价 说明
阅读成本 每处都要翻译
搜索失效 搜 user 找不到 member
重复实现 以为没有,另写一份
变更遗漏 改一处忘另一处

第四项(变更遗漏)有个具体形态:加了一个状态值,只改了 A 模块的判断,B 模块的 switch 落到默认分支——而默认分支通常是"当作正常处理",于是错误被静默吞掉。
第二项是最实在的:改名之前,"我要改所有用户相关逻辑"这件事根本无法用一次搜索完成。于是改动靠记忆,而记忆会漏。

第三项更贵:因为搜不到,所以以为不存在,于是同一段逻辑被实现两次,行为还不一致。

3. 词汇表的建立方式

做法 成本
一张 Markdown 表 极低
代码里的常量/枚举 低
类型定义(单一模型) 中
领域模型统一(DDD) 高

第一项最实用:一页纸,三列(概念、代码中的名字、禁止使用的同义词)。词汇表的维护成本要压到最低:放在仓库里、随代码评审更新、不追求完整。追求"一次写全"的词汇表往往写不完,而"只登记已经出现过分歧的概念"的词汇表可以持续长大。
它不需要工具支持,就能让评审时有依据。

第三项更进一步:让概念在类型系统里只出现一次,于是拼写错误与"另起一个名字"在编译期就会被发现。

// 词汇表(节选)

| 概念 | 代码名 | 禁用同义词 | 说明 |
|---|---|---|---|
| 平台用户 | user | member, account | 有登录能力的主体 |
| 组织成员 | member | user(在组织上下文) | 属于某组织,无独立登录 |
| 生命周期状态 | status | state, phase | 枚举:ACTIVE/FROZEN/CLOSED |
| 权限级别 | role | type, level | ADMIN/STAFF/GUEST |

4. 命名的三条硬规则

规则 目的
同一概念只有一个词 可搜索
一词只表示一个概念 无歧义
缩写只在词汇表里登记 可读

第一项(一词一义)在重构时最容易破:把一个类改名成更通用的名字,却没有检查它的语义是否也跟着变宽了。通用名一旦被复用,第二次复用就会带来歧义。
第二项比第一项更容易被违反:status 在 A 模块指"订单状态"、在 B 模块指"用户状态",两个都对,但混用时必须靠上下文判断。

第三项的失控很常见:cfg、conf、config、setting 四种缩写同时存在,而新人无法判断它们是否有区别。

// 反例:同一概念多种拼写
class UserCfg { }        // 配置用 Cfg
class AccountConfig { }  // 同类东西用 Config
class MemberSetting { }  // 又用 Setting

// 正例:一个后缀,一种含义
class UserConfig { }
class AccountConfig { }
class MemberConfig { }   // 至少读者知道"这三者同层"

词汇表还有一个"自动校验"的用法:把概念名做成常量或枚举,让"另起名字"在编译期失败。比如统一用一个 Status 枚举而不是各模块自己的字符串常量——于是"字符串写错"这类问题从运行时错误变成编译错误。

// 反例:各模块自己写字符串
if (order.getStatus().equals("ACTIVE")) { }      // 拼错不报错
// 正例:单一枚举,拼错即编译失败
if (order.getStatus() == OrderStatus.ACTIVE) { }

5. 改名的正确做法

步骤 动作
1 建词汇表 确定目标名
2 加别名(过渡) 旧名保留,委托新名
3 逐模块替换 每批可验证
4 删别名 确认无引用

第二项是关键:一次性全局改名风险高,而"加一层别名"能让新旧共存,逐步迁移。改名还有一类"看不见的引用"必须专门搜:字符串里的类名(反射、序列化)、配置键、SQL 里的列名、接口字段名、日志与指标名。前四项错了功能失效,第五项错了会"看起来正常但监控断掉"。

第四项不能省:别名保留太久会变成事实标准,于是"两种名字都对"的知识留在团队里,而不是代码里。

// 改名的过渡期(三周)
第 1 周:词汇表 + 新增 UserService.getUser(旧 AccountService.getAccount 委托)
第 2 周:调用方切到新名(按模块分批,每批一次提交)
第 3 周:删旧方法(先搜字符串引用,再删)
// 每批都可回滚;别名存在时间短,不固化

漂移的"检测"可以做成一次扫描:把同一个概念的所有候选词列出来,统计每个词出现的模块数。若同一概念分散在三个以上模块且用词不同,那它就是漂移最严重的地方,也是收益最大的治理点。

// 粗略统计候选词出现在多少个文件里(以"用户"为例)
for w in user member account customer; do
  printf '%-10s %s files
' "$w" "$(grep -rli "$w" src/ | wc -l)"
done
// 四个词都有几十个文件 → 该建词汇表;只用一个 → 无需处理

6. 排错:五种"命名导致的困惑"

现象 成因 处理方向
搜不到已有的实现 同概念不同词 建词汇表
改一处漏另一处 无单一来源 统一名字再改
新人反复问同一问题 词汇不一致 词汇表进文档
同名字段语义不同 一词多义 加限定词(order_status)
缩写看不懂 未登记 缩写入表

第三项(新人反复问)是词汇表最直接的收益指标:同一个问题被问三次以上,说明这个词该进表了。所以词汇表应当由"提问记录"驱动,而不是由架构师一次设计。
第四项的正解是"加限定词"而不是"换一个更花的名词":order_status 与 user_status 比 status 与 stage 清楚得多。第五项(缩写)有个实用上限:团队里超过三个人看不懂的缩写,就该写全。缩写省的是打字时间,花的是所有人的阅读时间。

7. 边界:什么情况不必强求统一

场景 为什么 做法
与外部系统对接的 DTO 对方命名固定 只做映射,不改名
数据库列名 改名成本高 用映射层隔离
第三方库类型 无法改 适配层
一次性脚本 生命周期短 随意

最后一条与"AI 辅助编码"有关的边界:助手会用"它见过的常见命名"补全,而这些名字来自成千上万个项目,不一定与本项目的词汇表一致。所以词汇表还有一个新用途——贴进提示里,作为命名约束。
判断口径一句话:同一个概念会不会被两个以上的模块用到。会,就必须统一;只在一个模块内,命名只是局部风格。


8. 自测

  1. 命名漂移的四种成因里,哪一种最隐蔽?为什么"旧名沿用、语义已变"比"多人独立命名"更难发现?
  2. "搜索失效"为什么是最实在的代价?它如何导致重复实现?
  3. 词汇表三列分别是什么?为什么"禁用同义词"这一列比"正确名字"更有用?
  4. 三条硬规则里,哪一条更容易被违反?status 在多个模块里分别表示不同含义该怎么修?
  5. 改名的四个步骤里,为什么"加别名"是关键?别名保留太久会发生什么?
  6. 判断"是否需要统一命名"的那个问题是什么?
S22-A16 · 命名的漂移:为什么代码里出现三种叫法
https://www.clxhxhhr.top/posts/6491/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。