☰
从“emmmm”到好命名:代码标识符的语义重构与团队规范
2026/10/15 6:47:00 网站建设 项目流程

"emmmm" 这个标识符,我第一次在代码评审里看到的时候,第一反应是去聊天窗口问同事是不是把草稿误传上来了。结果他说没传错,就是这个函数的真名:emmmm_check。写这段代码的同学后来解释,一开始只是临时验证个逻辑,随手敲了几下键盘,没想到后来越加越多,等想起来改名的时候,已经有点不敢动了。

类似的情况我在不同团队里见过太多次。程序员日常写代码,本质上就是不断生产标识符——变量名、函数名、类名、文件名、数据库字段名——然后让今天的同事和三个月后的自己来读。标识符起得烂,代码逻辑再漂亮也白搭,因为读代码的人第一眼看到的就是名字,第一印象定了,后面理解全靠猜。这也是我专门把"emmmm"拿出来聊的原因:它不是个例,而是烂命名最极端、也最典型的一个缩影。

这篇文章会从"emmmm"这类无意义命名出发,拆解命名这件事背后到底在解决什么问题,讲清楚一套从命名原则到实操重构的完整方法。不管你是刚入行的新人,还是正在带团队的老手,应该都能从中找到可以马上用的东西。

1. 一个"emmmm"式命名背后藏着的三个问题

1.1 命名首先是"检索信号",不只是给计算机看的

有一次我接手一个遗留模块,想找"订单校验"相关的逻辑,习惯性地用 IDE 全局搜索"check",结果跳出来 40 多个结果。其中一半是xxx_check,一小半是check_xxx,还有几个就是emmmm_check、emmmmm_check、emmm_check2。那一下午我都在点开文件看注释,整个人都麻了。

这就是烂命名最直接的成本:它把代码库变成了一个没法检索的黑箱。命名最重要的功能之一,是充当代码里的检索标记。这个标记越贴近业务语义,你找代码的效率就越高。一个叫validate_custom_rule的函数,即使没有注释,别人也能猜出它大概在干什么,并且可以用 "validate""rule" 这些关键词把它和其他代码块串联起来。而emmmm不携带任何业务关键词,搜出来全是噪音,等于是在一本技术文档里塞了几页无意义的填充词。

实操中我是怎么用的?每次新写一个函数,我会先想:如果三个月后的我要靠搜索关键词找到这段逻辑,我会用什么词来搜?把那个词直接放进函数名里。这个方法虽然土,但非常有效,能让代码库的"可检索性"始终保持在线。

1.2 名字承载上下文:为什么一看到名字就知道代码要干什么

很多人有个误解:代码是写给计算机运行的。计算机当然能运行不带语义的代码,但人没法维护。实际上,一个系统的维护成本大头都花在"读懂现状"这件事上,而名字承担了至少一半的上下文传递。

emmmm没有说清楚它到底是校验、是删除,还是临时跳过的占位。你要理解它,必须翻函数体,甚至去翻所有调用它的地方,把一段段逻辑拼起来。如果命名良好,比如is_order_paid,你根本不用看实现,就能参与业务讨论。

我印象很深的一个例子:一个叫emmmmm_handle的函数,实际做的是支付回调的活,调用方却一直以为它只是写日志。直到某次对账时发现数据对不上,一行一行追下去,才发现问题出在一个模棱两可的名字上。这类成本往往在几个月甚至一年之后才爆发,而且极难定位。说白了,名字是贴在代码上的说明书,说明书写错了,后面所有人都会走错路。

1.3 随手起的名字,会反过来塑造代码结构

烂名字还有一个容易被忽视的副作用:它会顺着时间推着代码走向腐烂。心理学里有个"承诺与一致"的倾向,人会对已经形成的决定产生维护心理。代码也一样——当你在某个位置写下emmmm的时候,你心里清楚它是临时的;可下一次再往这个函数里加逻辑时,你会想"这里本来就没法总结成一句话,那就继续堆吧",于是补丁越打越多。

反过来,准确的名字对结构是有约束力的。如果你把一个函数命名为calculate_order_discount,下次想往里塞"发送邮件"的逻辑,你自己都会觉得别扭,自然就会想着拆开。这也是为什么很多重构实践把"改名"列为最便宜、最安全的架构调整手段——它不改变任何运行行为,却能改变整个代码被理解的方式,以及后续被扩展的方向。

2. 命名不是挑单词:核心细节与常见命名法则

2.1 用"角色 + 行为 + 边界"三层信息搭出好命名

好的标识符不是一个单词的选取,而是一组决策的组合。我习惯把一次命名拆成三层来看:

  • 角色:这个东西在系统里是什么。变量是一个用户、一笔订单,还是一个状态标志。
  • 行为:函数对数据做了什么。计算、校验、转换、发送,还是读取。
  • 边界:它处理的范围或触发条件。是"已支付订单",还是"普通会员"。

把三层信息组合起来,可读性立刻不一样。比如emmmm_check是零信息;is_valid只是"有角色 + 有一点点行为",但没有边界;is_paid_channel_order则把"判断某个渠道的已付款订单"这个语义完整表达了出来。

这里有一套约定俗成的语法:变量名用名词短语,函数名用动词短语,布尔值用is、has、can、should开头,类名用名词且尽量不带动词。规则一旦固定,读代码的体验就像读文章,每个词承担什么语法成分,扫一眼就能判断。

2.2 不同类型标识符各有各的命名语法

数字字段别叫裸的value,要用amount、count、total_price这类自带单位或含义的词,否则你根本分不清它是金额、数量还是评分。状态字段要分清status和state;时间字段统一后缀_at、_time;枚举值要能看出完整的取值路径。

下面这张表是我经常用来给团队做培训时的对比素材:

反面示例问题在哪推荐示例
value不知道是金额、数量还是评分total_amount/item_count
flag什么条件?谁看了都懵is_enabled/has_permission
temp临时变量没有任何语义按真实含义命名,或直接提取成函数
data2数字后缀暴露了复制粘贴痕迹filtered_items
getProcessedData动词含糊,不知道从哪拿、怎么拿load_active_subscriptions

这里说的不是教条,而是"降低歧义"的通用底线。不同编程语言对标识符的语法约束也不一样:Python 社区用下划线风格,Java 系用驼峰命名,Go 社区刻意追求短名字。工具链不同没关系,真正要守住的是语义完整性——换个语言只是换一套外表,里子还是"角色 + 行为 + 边界"。

2.3 作用域决定名字长度:什么时候可以短,什么时候必须长

命名长度没有绝对标准,但有一条原则特别好用:作用域越大,名字越要完整;作用域越小,名字可以越短。

一个全局配置项、一个被多处调用的公共接口参数,名字宁可长到 30 个字符也不要含糊,比如max_retry_count_on_timeout虽然长,但一眼就知道是什么。而循环内部的下标i、j,或者列表遍历时的item,短就是合理的,因为它们的作用域就那几行,上下文完全锁得住。

缩写也一样:只有团队所有人都知道的缩写才允许使用,比如req、res、cfg。用自己的私人缩写去命名共享代码,就是给团队埋雷。我曾经见过pm在同一个项目里分别代表"per month"和"project manager",两个模块鸡同鸭讲了半年才被发现。

3. 实操演示:把一坨"emmmm"代码重构到能干净上线

3.1 典型病号:一段你大概率见过的"emmmm"级代码

先来看一段"病号"代码,它几乎集合了所有烂命名的经典特征:

def emmmm(a, b): a2 = [] for i in a: if i.status == 2: a2.append(i) x = 0 for j in a2: x = x + j.total if b: x = x * 0.9 return x

先别急着说"我不写这种代码",你回想一下自己刚入行时写的东西,或者翻翻某个历史遗留模块,大概率能看见它的影子。

这段代码的真实意图是:计算订单中状态为"已支付"的商品总价,如果传入的会员级别表示是 VIP,就再打九折。但你看代码本身,完全看不出这个意图。emmmm不说用途,a和b不说含义,2和0.9是魔法数字,i.status == 2更是让人一头雾水——状态 2 到底是已支付还是已取消?

3.2 先翻译语义,再翻译成名字

重构这种代码,第一步不是打开 IDE 改名字,而是先闭着眼把这段代码的功能用一句人话写出来。我当时的语义草稿是这样:

  • 输入:订单商品列表、会员级别
  • 输出:应付金额
  • 逻辑:先过滤出已支付的商品,再对它们的price * quantity求和,最后如果是 VIP 就打九折

把这句话写完,命名几乎就是填空了。函数做的事是"计算已支付订单的总金额",于是函数名出来了:calculate_paid_order_total。两个参数分别是"订单商品列表"和"会员级别",于是参数名出来了:order_items和member_level。过滤出的中间结果是"已支付商品",就叫paid_items;求和结果是"总金额",就叫total_amount。

3.3 重构前后对比,为什么每个改名都有依据

改完之后变成这样:

def calculate_paid_order_total(order_items, member_level): paid_items = [item for item in order_items if item.payment_status == PaymentStatus.PAID] total_amount = sum(item.price * item.quantity for item in paid_items) if member_level == MemberLevel.VIP: total_amount *= 0.9 return total_amount

逐行对比一下:

  • emmmm→calculate_paid_order_total:把"计算已支付订单总金额"这个完整意图放进了函数名里,阅读者不看函数体也能知道你调的是什么。
  • a→order_items:参数名直接说明这是一个订单商品列表,函数签名自带类型语义。
  • b→member_level:调用方看到这个参数就知道要传会员级别,不会传一个布尔值进来。
  • a2→paid_items:中间变量名对应过滤动作的结果,别人能顺着"paid"这个形容词迅速理解它是"筛选过的对象集合"。
  • x→total_amount:金额变量用明确的名字,而不是一个毫无信息的字母。
  • i.status == 2→item.payment_status == PaymentStatus.PAID:魔法数字用枚举代替,代码自己解释了自己。

改完之后的代码,逻辑一模一样,行为没有任何变化,但阅读成本和前一份已经是天壤之别。

3.4 重构纪律:只改名不动逻辑,成本最低

这里要强调一条我在实践中反复用到的纪律:命名重构时,一次只做改名,不要顺手修逻辑。原因很简单——改名的风险窗口很小,你只要保证新旧名字指向同一段行为,测试过了就是过了;但你要是同时把循环改成列表推导、把魔法数字改成枚举、还把折扣逻辑调整了一遍,出问题时根本分不清是哪一步引入的。

我每次改这种代码会按五步走:

  1. 先用一句话写下这段代码的真实语义。
  2. 在改动前建立测试用例,哪怕是临时的"前后输出一致"断言。
  3. 用 IDE 的"重命名符号"功能批量改名,而不是手动全局替换,这样不会漏掉某个调用点。
  4. 逐行跑一遍 diff,确认每一处变更都只是名字变化。
  5. 全量测试通过后,再把临时的语义注释补成正式注释。

在这个流程下,一次命名重构通常只需要几分钟,但它对后续维护效率的提升是持续的。我叫它"最便宜的转型训练",因为它没有架构风险,却能让代码的结构感一点点回到正轨。

4. 常见命名问题、排查方法,以及四个经典误区

4.1 一条命令扫描代码库里的"emmmm"

如果你想知道自己的项目里到底有多少这类占位型标识符,可以用一段简单命令先做个体检:

grep -rE '\b(emmmm*|temp|tmp|test1|aaa|bbb|data2)\b' --include='*.py' .

这个命令会扫出常见占位符、无意义拼音、数字后缀变量。注意它会有误报,比如temp在温度系统里可能真的是温度缩写,所以扫出来后要人工过滤一遍。更系统的做法是把这类词整理成一份"禁止列表",配置在团队共用的 lint 规则里,从源头拦截。

除了搜索占位符,还要重点留意这么几类现象:拼音混排的标识符、只用一两个字母且无法从上下文中猜出含义的变量、带数字后缀的变量、名字描述的动作和函数实际行为不符的函数。这几类几乎可以覆盖 90% 的命名坏味道。

4.2 Code Review 里讨论命名的高效姿势

命名问题在 Code Review 阶段很容易演变成"我觉得好看、你觉得不好看"的争论,非常消耗团队情绪。我的经验是,讨论命名时只围绕一个标准:读者能不能快速正确理解。只要基于这个标准,讨论就能落地。

具体操作上,我习惯用三句话模板:

  1. "这个函数名让我以为它做的是 X,但它实际做的是 Y。"——先说误解,再说事实。
  2. "如果改成xxx会不会更贴近它做的事?"——给替代方案,而不是只批评。
  3. "这个名字在搜索时应该用什么关键词?"——把问题从审美拉回到可检索性。

只要大家承认"名字是沟通工具",大部分命名争议都能在五分钟内解决。怕的是把命名当成个人风格宣示,那就没完没了了。

4.3 静态检查工具能抓住什么,抓不住什么

lint 工具、IDE 自带检查、静态扫描平台,这些自动化手段能解决一部分命名底线问题,但解决不了全部。它们能抓住:未使用的变量、同作用域重复命名、完全不符合命名规范的风格问题、明显的魔法数字。这些是"底线"。

它们抓不住的是真正的语义问题:getAllItems到底是拿全部订单还是全部商品?processData里的 Data 到底指什么?这些需要 review 的人去判断。所以我的建议是:用自动化工具守住格式底线,用 Code Review 和命名规范守住语义线,缺一不可。

如果你用 Git 做版本管理,还可以在提交前加一个简单的钩子,把明显的占位符拦在门外。比如:

# .pre-commit-config.yaml 片段(示意) repos: - repo: local hooks: - id: no-bad-naming name: no bad naming entry: grep -rE '\b(emmmm|tmp1|data2)\b' language: system files: '\.(py|js|ts)$'

这个钩子的作用就是在代码进入仓库之前,给占位符类标识符踩一脚刹车。它不能代替人,但能把最刺眼的垃圾拦在外面。实测下来效果很稳。

4.4 四个关于命名的经典误区,我都踩过

第一个误区是"名字越短越好"。短名字只在小作用域里成立,一旦跨函数、跨模块,短名字就会变成没有上下文的字母游戏。a、b、c连写,等三个月后回看,谁也记不住哪个是哪个。

第二个误区是"所有方法都叫 doSomething"。如果你满屏都是doWork、doCheck、handleData,那和叫emmmm没有本质区别。动词本身没有信息量,真正有意义的是动作的对象和边界。

第三个误区是"看到烂命名就急着全量重命名"。重构风险看着小,但如果你一次性改几百个文件,又没有足够的测试保护,很容易把调用关系改断。我建议按模块小步推进,一次只改一条调用链。

第四个误区是"把命名当成纯个人风格,拒绝团队统一"。命名规范的价值不在于审美统一,而在于降低沟通成本。如果每个人都按自己的口味来,代码库就是一本没有目录的词典,查什么都查不到。

5. 团队命名规范怎么落地,才不会被当成形式主义

5.1 命名规范不是架构师拍脑袋定的

很多团队搞命名规范,最后变成一纸空文,原因是规范由某个人凭空写出来,跟真实代码库里的痛点一点关系都没有。

我自己参与过一套比较成功的命名规范的起草过程,它的来源是两条线:一条是从现有代码库里收集的"烂命名清单",一条是从线上事故和维护痛点反推出来的"容易出歧义的词"。整理完之后,规则才被总结成文件。这样的规范才有说服力,因为每一条背后都有真实案例支撑。

规范文件的结构可以很清爽:第一部分是通用原则,比如"命名要能承载完整语义";第二部分按变量、函数、类、常量分别给示例;第三部分是禁止清单,把emmmm这类占位符和容易混淆的缩写列进去;最后留一个"团队术语表",把业务里的关键概念统一起来,避免同一件事在不同模块里有五套叫法。

5.2 把命名检查变成 MR/PR 的必过项

规矩定得再好,不执行也是白搭。我见过最有效的落地方式,是在 MR/PR 模板里加一个勾选项:"我确认本次变更中的标识符符合团队命名约定"。别看只是一个勾选框,它逼着每个人在提交之前至少想一遍自己的命名。

还有两个做法也很实用:一是让新人在入门培训里做一次命名重构练习,把一段emmmm风格的代码改成规范版本,既可以熟悉代码库,又顺便把规范学了;二是定期开一次"命名重构专场",不动逻辑,只改名字,合并后跑全量测试,通常半个工作日能覆盖一个模块。这类专场对代码库的长期健康帮助非常大。

5.3 规范要留出口:别让命名变成新教条

命名规范一定不能变成教条。比如循环里的i、j,比如某个团队里用了五年、所有人都明白的缩写,规范不该强行要求改掉。规范存在的意义是减少沟通成本,如果某条规则让所有人写代码时都难受,那问题一定出在规则本身,这时候要去修订它,而不是硬扛。

另外,不要在代码评审时揪着"这个变量名我不喜欢"反复打回。命名讨论要往"可理解性"和"可检索性"上引,往"团队术语表"上引,而不是比谁的词汇量更大。好规范的标准很简单:新人看了能迅速写出大家看得懂的代码,老手看了不会觉得被束缚。

最后说一点我个人特别深的感受。前几年我给自己立过一条规矩:每写一个标识符之前,先从读者视角想一下——如果有个完全没参与需求的人来看这段代码,他能不能只看函数签名就大致猜出这段代码在干什么。如果猜不到,我就停下重新想。遵守这条规矩一年后,我发现自己很少再写"本来想临时用、后来成了永久"的补丁式代码了,团队接手模块时的沟通成本也明显降了下来。命名的功夫不在手上,在脑子里;你愿意为下一个读代码的人多想几秒,整个项目的回报都是长期的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询