在编程实践中定义变量时,我们所能控制的无非两点:变量类型与变量名。某种程度上,这两者分别考验的其实是开发者的数学水平与语文水平。在今天,即便已经有了非常高大上的类型系统,「名不副实」的变量名仍然经常能对开发者造成困扰。那么,我们有什么理论能用来指导变量命名呢?
在计算机科学的萌芽时代,变量名和变量类型之间并没有明确的界限。比如,著名的「匈牙利命名法」提倡的就是把属性 + 类型 + 描述一股脑地写进变量名:例如 const int max 就对应 c_i_max 的名字。编程语言发展到今天,这种实践看起来自然就很别扭了。对于现在的主流编程语言,我们大可以认为,它们中的变量名就应该是纯粹的「对变量的描述」。
那么,怎么样描述好一个变量呢?这时,我们的关注点其实已经从自然科学中的类型理论转移到人文科学中的文学创作了 :) 在给变量取名时,我们实际上要做的是言简意赅地描述清楚一个抽象的东西,这其实并不是件容易的事情——「名字」可是万恶的自然语言的范畴里的东西,试问列位能在白板上手写 bug-free 红黑树的大神们,可曾用母语写出过满分的高中作文?
所以,自然语言在编程中其实发挥着非常重要的作用——毕竟代码是写给人看的。笔者相信,每个正常的阅读者,都会在潜意识里用从小耳濡目染的自然语言来理解代码逻辑,然后才是套用(也许是培训班昨天才教的)编程语言语法规则来考量细节。这里,某些标榜着简洁但充斥着大量符号的编程语言就可以作为反例:用它们写出的代码也许对于热爱推导公式的 Geek 来说非常友好,但普通人第一眼看上去就是天书。当然,矫枉过正的例子也不是没有:对于一些鼓励超长变量名的语言,维护它们的代码……多少会有些捏着鼻子的感觉。
到此,我们已经总结出了变量命名的若干微妙之处:
- 变量命名与类型系统是分离的。
- 变量命名依据其实是自然语言。
- 命名过于简洁时,可读性不好。
- 命名过于冗长时,可读性也差。
听起来是不是很主观,很难量化呢?这就引出了我们的主题:虽然命名难以量化,但你可以依据自然语言的语法,来整理出一些通用的规则呀 :)
语法是笔者高中时代比较反感的东西——你不学它,光凭「语感」也能中个八九不离十;你学了它,反而可能稀里糊涂套错场景……不过,语法其实很像自然语言的类型系统,它与编程语言中相应的概念有着很微妙的联系。比如,JavaScript 中的关键字,就可以根据语法中的词性来这样粗略地分类:
名词
function var class
动词
import export extends return break continue
delete switch new try catch throw yield
介词
for in else
连词
if while
代词
this
复制代码
是否能够注意到动词明显地比名词更多?而且,编程语言中还有不少介词 / 连词这类的虚词来表达控制流。相比之下,HTML 则几乎就是名词的天下:吃我 head body form button 啦!HTML 无非是一堆名词的堆叠嵌套,相比存在着复杂逻辑的编程语言来说,较少遇到与命名相关的维护性问题。那么,在维护编程语言的代码时,有哪些自然语言的规则能够有助于提高可读性和可维护性呢?这里笔者总结了这么几点:
- 注意命名与类型在语义上的匹配
- 保持概念的一致性
- 避免制造出反语言直觉的结构
- 慎用宽泛的抽象概念
让我们逐条看一下吧 :-D
注意命名与类型在语义上的匹配
前面我们已经知道,词汇的词性和变量的类型有着一些微妙的关系。在命名时,保持这种关系的匹配有利于增强可读性。注意,这并不是像匈牙利命名法那样把类型写入变量名,而是根据类型来选择更契合的变量名:
- 对于布尔类型的变量,其命名常用形如
isSomething
/hasSomething
的形式。这其实非常接近自然语言里的陈述句——陈述句有着肯定和否定的形式,这就暗合了布尔值的true
和false
。 - 对于数组类型的变量,其命名常用复数形式。例如
apples
在自然语言里就暗示着有不止一个apple
,这和数组的思维模型是非常契合的。 - 对于对象类型的变量,其命名常用名词性短语。例如
UserModel
这样的 class。别忘了,名词和动词都属于实词,不过二者一个更加「面向对象」,一个更加「函数式」罢了。 - 对于函数类型的变量,其命名常用动词,或者说更接近祈使句式。一个函数就应该明确地去「做某件事」,这时候「说什么就做什么」显然更加简单真诚。比如,你可以尝试给你的类方法前面都加上个 Please,这时候
getUserData()
就能无缝地变成通顺的Please get user data
的祈使句了。当然,在函数式编程中,面向函数耍出的各种花样,还可以用各种虚词来粉饰。譬如像withState
这样的名字,其中就暗含着对参数的「修饰」能力。再有各类回调的场景,也常见 on / before / after 这样的介词来表明相应动作的触发时机。
不同的编程语言和范式,在词汇的选用上会有非常鲜明的区别。比如 Java 就是个名词王国,而函数一等公民的语言里,动词的地位就高得多。这里的风格建议是在什么地方就按什么地方的规矩来说话,这样到哪都吃得开 XD
保持概念的一致性
在有了 Lint 等格式化工具之后,项目里的换行缩进一般都能得到统一,这确实能消除代码格式排版上的潦草感觉。但是,许多项目里仍然很常见这样的地方:
- 三个类似的操作分别封装成
loadUser
/fetchUser
/getUser
三个不同的函数,它们好像都是一回事,可是你敢随便换着用吗? - 上面一个
elements.forEach(element)
,下面就是elements.forEach(elem)
。一个 index 也可以有i
/idx
/inx
/ind
四种不同的缩写 :) - 现有模块中私有变量按照形如
$xxx
的格式声明,新代码上来就是个__xxx
。
其实这些问题单独拿出来,都很难称之为严重,相应的解决方案应该也都是老生常谈的了。但如果这样的不一致性遍布项目的代码库,那么再工整的空格缩进,也掩盖不了代码的杂乱感。这里的一个非技术建议是建立 Code Review 机制:这些问题很难单纯通过 Lint 工具来约束,提出 Review 意见与修改也并不难,关键在于流程:
- 新人很难一上来就了解各种隐式的约定,许多老成员轻车熟路的实践,对于新人来说反而是个不熟悉的暗坑。
- 一旦代码并入主干,再去修改它的成本就会显著地提高——老代码跑得好好的,重构坏了谁负责?
当然了,Code Review 还有许多其他的功效,这里就不再展开啦。
避免制造出反语言直觉的结构
我们已经提到过,直觉在编码中发挥着潜移默化的作用。那么,什么叫做「反直觉」呢?譬如这么些小地方:
- 双重否定表示肯定,那么我们就应该放心地用 2N 重的否定来表示肯定吗?机器当然能正确地理解
if (!dataNotLoaded !== false)
的精妙逻辑,不过这时候恐怕很容易把自己和别人绕进去,尤其是在与或条件多起来的时候 :-p - 把数据「封装一层」是很常见的手法,不过这时候如果出现了
data.data
,该怎么确定其它代码中用到的data
变量指的是谁呢? - 深度嵌套和花式跳转等形式的控制流也是反直觉的,但是这已经超过变量命名的范畴了 XD
慎用宽泛的抽象概念
得益于自然语言中丰富的抽象概念,总有一些变量名是非常「方便偷懒」的。比如,如果你实在想不通一个装数据的变量该怎么命名,那就叫它 data
啊!如果一个基类不知道叫什么好,那就叫它 Base
呗!
其实,如果是因为「没有想清楚该起什么名字」而使用了这样宽泛的概念,实际上就会在暗中欠下「没有清晰正确的设计」的技术债。比如上面的行为,在扩展或重构时很可能遇到这样的问题:
- 到处都是
data
,以至于不仅没法简单地查找与替换来重构,依赖 IDE 来改个变量名都要提心吊胆,不确定影响范围。 - 各种形如
core
/base
/common
的模块,难以界定边界在哪:core 到底管些什么事,继承它到底会带来哪些副作用,而我的新函数要不要放在里面呢?
对于装数据的变量来说,data
/ item
这样的名字写了和没写差不多——当然了,对于可以多处复用的工具函数,这样的命名完全没有问题。相对地,对于函数来说,各种具体的动作就比较容易描述清楚,不过你如果非要把所有的回调名一律写成 callback
,那也不是不可以……
其实上面的这些问题,很多只要在提交前 double check 一遍流程,就能够在自省中发现。这也是一个非常重要的技能:找出自己哪里做得还不够好,本身就是一种进步了。
总结
对各种概念的抽象过程常常是编程中最有乐趣的地方之一,而好的变量命名无疑有助于将思维过程更清晰地表达出来。在本文提出的观点里,变量的命名之难,与类型的强弱和类型系统的动态静态并没有直接的关系:数学大师的语文未必就足够好,反之亦然。我们也基于一些自然语言里非常简单的规则来提出了一些对编码实践的建议。不过,如果下次遇到 diss 你变量命名的 review 建议的时候,本文也可以为你提供一个有力的反击论据:
我们连编程语言的类型系统都很难完全搞明白,更何况自然语言的呢?