命名实际上很难
最近因为又翻到了自己以前的老代码,看得时候相当头疼,结果让 AI 扫一下一下子就清楚很多。去想原因,第一感觉可能想到也许因为现在懒得看代码,对代码库的接触时间不够长不够熟悉,才会出现看着陌生的情况;但仔细思考后,发现实际上有另一个更深的原因—— 命名太烂了。
不要误会,我当然不是那种 x 或者 hd 之类的命名规则破坏者或者缩写狂魔,反而我已经好好遵循了语言所需要的大驼峰、小驼峰、下划线之类的命名规则。但为什么我还会说命名烂呢?
因为命名的内容很糟糕。比如说 OwnerInventory 是一个类名,光看类名可以知道它应该是一个 Inventory 之类的容器,但是 Owner 是什么?这个 Owner 是对谁而言?完全不知道。而实际上这个是用来装载卡牌游戏中各个选手卡牌的数据容器。
另一个例子是 AffectedTurn,这是一个状态机的某个状态类,继承自 GameTurn 这个状态机的状态基类,但是完全不能一眼看清楚 Affected 具体是做什么。实际上这个类是用来让卡牌效果进行生效的特殊的状态回合。
实际上这是大驼峰小驼峰烂大街导致的后果,对变量的命名太过于关心在大驼峰小驼峰之类基于类型的命名规则上,反而会让人忽视了用词和选词本身对变量上下文提示的重要性。
举例说上面的命名虽然遵循了大驼峰之类的基于类型的规则,也只能享受大驼峰这类规则带来的信息,根据名字一眼看出方法还是类、变量。但对于类具体是大概做什么的,则完全是混乱的。
这显然和最初使用命名规则来追求「命名即注释」的代码美学是相悖的,遵循基于类型的命名规则不是命名的所有,而是一部分,烂大街最好做到的那部分。
我在查阅资料的时候试过,搜索引擎搜到的大部分都在讨论大驼峰这种类型命名规则,反而是对变量用词上说「使用清晰的变量名」这样看起来很废话的模糊描述。不过正因如此我才需要写这样一篇文章。
因此,我不会在这篇文章讨论大驼峰小驼峰这种烂大街的东西,而是从概念开始,讨论如何使用和看待动词、介词、冠词,最后总结出一系列命名建议。
概念
其实命名本质上就是在定义概念。一个变量名,就是一个概念的缩写。如果概念本身是模糊的,那名字必然也是模糊的。
在 DDD(领域驱动设计)中,有一个很重要的动作叫「和领域专家对齐用语」,对齐的结果就是一份概念清单,里面记录了项目里每个核心术语的精确定义。
「概念」的用途是校准整个项目的用语,而制造一个「概念」大概需要的是几样东西:
- 名称,用来作为概念的名字被使用。
- 以及说明描述,用来描述这个概念是指什么。
- 避免使用的近义词,比如说既然有了「手牌」这个名字,就不要使用「卡牌」这个词来描述了。
制造一个概念很简单,比如说这样:
PlayerHand: 玩家的手牌.
Avoid: Deck, CardContainer, Inventory这样一个概念就知道不要使用 Inventory(背包) 来讨论,而是使用 PlayerHand(玩家手牌)来进行讨论。
这对于变量命名的启发便是:使用一致准确的词语,避免同时使用一对不同的近义词来命名变量。
当然我们也可以借用已经有的通常的概念,比如说 Terminal 通常都会指向运行程序的终端,而 Console 指的是能够在运行时进行动态控制的事物。(虽然有人也用来干形容别的)
动词
概念更偏向创造名词,虽然概念的技巧也可以用于创造动词,但对于绝大部分场景来说,变量所需要的动词几乎都已经有人创造过通用例子了,所以与其去麻烦地创造新动词概念,不如直接考虑现有常见的动词。
最常见的比如说 Enable / Disable 或 Active / Deactive 。但由于上面说过概念避免同时使用近义词,因此最好从这两对相近的近义词中选择一对固定使用——因为它们的概念实际上都是一样的,Enable / Active 表示持续发挥功能,Disable / Deactive 表示持续不发挥功能。
此外也有常见的动词概念包含了意义:
Get 通常表示简单的获取,而 Find 表示比较复杂的,需要用算法来进行的搜索。虽然两个都是获取某个对象,但是蕴含的含义不同。
甚至 Try 前缀的加入就能代表了相当大的信息量,例如 C# 中,Try 通常会配合 out 参数来使用,同时也默认了返回值代表方法是否执行成功。
同时在使用动词的时候也需要注意动词的粒度,比如说 ProcessData 用的这个 Process 完全不知道在做什么,如果数据有要好几个函数处理,中间这个 Process 就很让人疑惑——这个函数到底把数据处理到那部分了,处理成什么样了?
介词
对我纯凭经验来说,如果能不使用介词,那就尽量不要使用。因为这三个会引入复杂的英语语法规则,你也不想在命名的时候要想到底使用 at 还是 in 吧?
因此使用它们时需要避免滥用,那么什么样的时候是必要的?比如说最经典的 Send 和 SendTo,如果没有介词就分不清哪个是广播哪个是单播消息。
知道是否需要使用介词的最简单的办法就是尝试把介词去掉,看看是否会产生模糊歧义之类的情况,如果会那就添加介词,如果不会那就放弃介词。
比如说 StartGameWith,去掉之后是 StartGame,没差别,那就去掉 With。
更变态一点的,比如说 SendMessageToPlayer,如果去掉介词就是 SendMessagePlayer,这会产生模糊,虽然你可能觉得「这不很清晰:发送、消息、玩家,连起来就是发送消息到玩家」,但是从另一个角度看却可能是 Send 出去 MessagePlayer 这个类,而刚好 MessagePlayer 又有可能是 PlayerMessage 的经典倒置例子(因为有些库可能有很多 Message,为了便于代码提示所以把 Player 后置了),那么 SendMessagePlayer 就会被人理解成 SendPlayerMessage,加上省略 To 代表广播的惯例,进而以为是广播玩家的消息,而不是单播消息到玩家。这个例子虽然可能有很多刚好,但足以证明模糊本身是存在的,甚至模糊后的结果刚好还能被解释,产生误解。
冠词
最好不要用. 引入冠词带来的信息量实在是太小了,还可能会导致代码补全补不到。
总的来说
实际上还是一个比较经验和玄乎的办法,因为变量命名本质上是根据经验来连接以前看到的概念,就像不知道句柄的人不懂 Handle 应该关心释放,Context 则表示有一个大会被多个函数修改的共用状态。
不过上面的讨论还是有一些实践建议:
- 可以使用通用惯例比如说 Terminal、Debug 这样的惯用词来为变量名称提供丰富的信息。
- 避免使用过多自创概念,除非确实是应用场景独有的。
- 用词避免使用近义词,代码库统一使用一套词。
- 合理使用介词来提示方法需要的参数情况。
- 冠词最好不要用。
回到开头的那两个抽象命名,在思考后,我觉得对 OwnerInventory 这种坏命名来说,统一使用 PlayerHand 或者更通用惯例的 CardInventory 更适合。AffectedTurn 则应该使用 EffectApplyingTurn 这样的命名更清晰表达 Turn 到底在做什么。
借此可以有最重要的实践建议:在命名上多用点用心。 花费些时间思考命名反而省去了后期详细写注释解释以及来回翻代码的麻烦。毕竟没人想在调用方法的时候还去翻具体实现研究在做什么。