变量命名是一种哲学。这句不只是感悟,不只是把说不透的复杂事物,全抛给哲学。
而是真正意义上的哲学,有哲学技巧可循、有实践指导的哲学。
程序在机器上的执行不需要用到变量名,在人脑里的执行需要用到变量名。
人脑执行程序,遵循心理学的认知规律,因而跟程序的机器执行特征不完全相同。
同一任务,可以由不同编程语言解决,在同一编程语言里也可以用不同方式去解决。这是一个一对多的关系。
然而,只有极少数的代码实现,遵循人的认知规律,从而表现为——可读性更好。
当然,这里的认知规律是一个立体模型,不是片面的:
-
不学习、不动脑就能阅读的代码就是好代码;
-
我觉得容易阅读,通过同理心,我想象不如我的人也容易阅读的代码,就是符合认知规律的代码。
人类是善于学习的动物,学习本身也可以是有趣的过程。在代码设计里追求学习,以及促进他人去学习是可行的,试图更充分发挥认知规律的策略。
因此,一个乍看不容易理解的代码,不一定是可读性不好的代码,在经过学习后,不容易理解的代码,可能变成非常直观的代码,非常优雅的代码。这种学习体验在数学公式里司空见惯,代码本质上就是更琐碎的数学公式。
那么,人类的哪些认知规律,可以帮助我们提高代码的表达力?如何在变量命名技巧上得到体现?
1、人类喜欢重点,且是关健具体的重点
孤帆远影碧空尽,惟见长江天际流。
雷军说写代码要像写诗一样,这不是一个空泛的表达。代码里的变量名,就像诗歌一样,略去不必要的细节,保留最关键的词汇,并且越具体的词汇,比模糊的、不准确的越好。
多写写诗歌,或者训练一下文言文,增加语文功底,有助于提升变量命名的技巧。
2、人类喜欢分类,特别是视觉上分类
代码编排也有视觉效果,对同一资源相同的处理可以放到一起,同一种处理也可以放到一起,同一类处理也可以放到一起。
有时,不同分类之间有冲突,不能同时满足。此时需要找到关键分类,以它为主,找不到就多试试,多调整,感受一下变化。
变量命名上,尽量把同一类的聚合到一起,在视觉上、概念上,它们可以作为一个视觉组块被加工,减少工作记忆里的占位空间。
此时,分类良好的纯函数有很大优势,纯函数的代码模式很同一,函数体里先是一对 get 性质的赋值语句,然后 return 一个表达式。
get -> return 模式,让开发者在跳转到函数定义时,内心已经有预期(preload 了相关代码识别的心理框架,加入代码阅读效率,维持心流)。
3、人类喜欢分层,特别是层层递进的分层
当事物本身很复杂时,我们喜欢看到事物的层次,每一层都是简单的,组织起来变得复杂,但此时的复杂背后拥有了简单,显得不那么复杂了。
-
空间维度的分层,主要是分块,把一个大任务,分块成几个组成部分,每个组成部分细化处理。
-
时间维度的分层,主要是分阶段,把一个大任务,分成多个阶段,每个阶段细化处理。
-
概念维度的分层,主要是从特殊到一般,把一堆具体任务,推广到更一般的场景,然后通过参数化的方式,再细化回来。
三种分层方式,提供了一个变量命名的框架。
-
xPartOfThing 表示空间维度分层后的 x 部分,还可以有 yPartOfThing 等,也就是说,我们可以在不具体命名时,先给个占位。我们只知道 Thing 层面,对它的细节组成部分认识不够,不知道叫什么好,就可以临时叫 xPartOfThing,然后等代码实现推进,对组成部分认识更多,可以分配更具体的变量名了
-
xPhaseOfThing 表示时间维度分层后的 x 阶段,跟空间维度分层同理,等后面细化具体阶段的名称
-
xLayerOfThing 表示概念维度分层后的 x 阶层,等对问题认识更清楚后,再分配新名称。
看到这里,有读者可能疑惑,原来不是要一次性命名了事,还得不断重构代码,调整变量名?
没错,优雅的代码不是一气呵成的,也不追求一气呵成。软件之所以是软件,而不是硬件,其优势就是可编辑性更强,可以反复调整、更新、迭代。
不只是新需求促使我们修改代码,对问题的新认知也可以。
不必吝啬于修改变量名。
4、人类喜欢自圆其说
我们在自圆其说时,头脑仿佛被激活,富有创造力,试图构筑更完满的结构,并感到快乐。
代码里如何自圆其说?按照领域 (domain) 组织我们的代码,产出接口(interface)。
这种代码组织方式,可以引导开发者围绕特定 domain 做上述(空间、时间和概念)分层的处理。
此时,变量命名的灵感犹如泉涌,免费附赠。
比如写 web framework 时,处理 Server 相关的 Domain,HttpRequest, HttpResponse, HttpHeaders, HttpMethod, Url, JsonResponse, HtmlResponse, FileResponse……
很多领域相关的要素,会启发开发者的变量命名。但是,只有在我们按照领域(domain)去组织代码时,才有显著效果。单纯按照编程任务的线形执行过程,去一一实现和解决,会遭遇多个领域的组块、阶段和概念交织在一起。
这种交织是不利于我们聚焦任意单一领域去做更全面认识和统筹,并且它还极大地增加了认知负担,性质不同的事物很难在认知上构成一个记忆组块,很容易占用过多的工作记忆。
每次聚焦单个领域时,该领域内部的分层和分块及其关系,会更自然地在认知里融洽起来,被当作整体处理。我们自圆其说的倾向,会关注该领域里的对称、对偶、闭环等结构上或层次上的完整性。
比如有 open,就想到 close,有 get 就想到 set,有 add,就想到 delete 等等。这些常常一起出现的各种操作,都是赠送的变量名。
更重要的是,对多个单一领域的自圆其说处理,是在为我们的代码库(codebase)增加优质代码资产,很多底层领域问题已经被解决,写代码时可以聚焦 high level 的业务逻辑。
5、人类也喜欢抽象
在前面我说人类喜欢具体,但这里又说人类喜欢抽象,这矛盾吗?
并不矛盾。
很多时候,人们误用了「抽象」这个词,把模糊的、不准确的东西,称之为「太抽象」。
很多时候,人们误会了「抽象」这个词,把自己没学会、没理解的东西,称之为「太抽象」。
人类喜欢具体事物,不意味着不喜欢抽象事物。人类更擅长加工具体事物,是因为具体事物的刺激,在日常生活中更常见,而抽象事物的刺激,则需要额外创造。
也就是说,抽象事物的刺激源比较稀缺。
这里我们需要引入「专业训练」的概念,所谓的专业训练,就是加大特定领域的抽象事物的刺激水平,让我们对特定领域的术语,也建立更好的加工水平。
大多数程序员,已经经过了基本的编程知识的专业训练。对变量、函数、循环、类、继承等术语,相比普通人而言具备较好的加工效率(感到直观)。
但这对写出优雅的变量名,写出优雅的代码而言,还不足够。
优雅的代码,往往包含两个核心领域的抽象知识:
- 代码领域
- 问题领域
开发者,不仅需要掌握编程知识,也需要掌握其所面对的业务领域问题的知识,并且要将两者有机整合。这是领域驱动设计(DDD)的战术部分,如何用程序设计复刻业务领域模型里的概念及其组织关系。
不只是代码领域有框架,非代码领域,也有框架。比如数学是所有领域的知识框架。
即便是数学,它也有自己的细分,其中甚至有数学的数学性质的细分领域,可以称之为数学理论的框架,比如范畴论(Category Theory)。
学习使用框架,可以帮助我们更快速地理解和解决特定领域的问题。
而学习框架的框架,可以帮助我们更快速地搭建一个特定领域的框架。
框架和框架之间,也有相似之处,也可以被统筹和有机组织起来。
比如,对资源的增删改查 (CRUD) 模型,就是很多具体业务领域模型的框架。很多问题,都可以被建模为对特定资源的管理过程。
对资源的 CQRS 处理,是在 CRUD 模型上的进一步细化。它的贡献主要是,分离了 read/write 模型,强调的是技术领域和业务领域的分层。CRUD 是技术领域的概念,约束少,而业务领域的模型,往往要求更多约束,并非随意增删改查,因此围绕 CRUD 不容易聚焦领域问题,而 CQRS 更强调要合乎业务模型的方式构建 query model 去查询资源,构建 command model 去更新资源。
这些事物被认为是——架构。
- 框架是对特定领域问题的抽象
- 框架的框架(架构)是对多个领域问题的组织关系的抽象
- 框架的框架的框架(体系)是对多个组织关系的组织关系的抽象……
- 框架的框架的框架的框架(范式)是对多个组织关系的组织关系的组织关系的抽象
- 四阶框架是对三阶框架的组织关系的抽象
- n 阶框架是对 n-1 阶框架的组织关系的抽象
人类喜欢抽象,喜欢看到上面这种分成多个并列小结,每一节都是前一节的某种递进抽象,包含特定模式的事物(本质上是归纳法)。
人类喜欢抽象,但抽象能力需要专业训练,专业训练过程是学习的过程,学习过程往往包含很多困惑和苦恼等阻塞。我们常常因此误会自己不喜欢抽象。
当我们成功掌握某种层次的抽象时,我们会爱上它们。
掌握越多层次的抽象,写代码命名时越有章法,越是轻松。
我们可以先用 getResource, setResource, queryRespurce, sendCommand 等更宽泛的抽象词汇去编写代码,然后再代码写就后,替换为更具体的操作名和资源名。
也就是说,抽象的框架让我们代码命名的收敛速度更快,我们不必因为不知道具体名称而阻塞,我们虽然不知道具体的,但我们知道它一定是属于某种更宽泛的概念 / 资源 / 模式的一种,我们先用抽象的做占位,继续展开代码。等事情完成,我们再回来一一分配新的变量名称,此时我们对问题认识更加充分和全面,更知道每一个变量名在整个代码图景里的位置了。
总的而言,人类喜欢抽象,不喜欢的是自己没掌握好的抽象。
6、人类对趋势和目标敏感
人类是善于预测的动物,我们总是在试图通过有限线索预测事物变化,并因为事物符合我们的预测而感到快乐。
这个特征如何用以优化变量命名?
把趋势、目的、目标体现在变量名(特别是函数名)里,表达资源变化的意图。
比如把 setCount(count + step) 这种通用数据更新的处理,封装成 increaseBy(step) 表达数据变化的意图和趋势,同时它的领域相关操作又会浮现出来,比如 decreaseBy(step),setMax(maxValue), setMin(minValue), animateTo(targetCount)。
只有 Count Domain 脱离简单通用技术语义,才可以做到有 min, max 约束,CountDomain#increaseBy(step) 是在特定领域上下文背景下的增加,可以抛弃掉溢出的值。
而 setCount(newCount) 的纯技术语义,很难允许 count 竟然不等于 newCount,除非还函数返回 boolean 等信息,告诉外部更新结果是否成功,以及更新失败的原因。
添加意图到变量名称时,常常会让变量名显得很冗长。这是很难避免的,需要经过大量训练,提高提交第一条说的「关键具体」要素的能力,即写诗的能力。
7、人类喜欢诚恳的事物
什么是诚恳的代码,说到做到,说了什么做的就是什么,是为诚恳。
很多代码的变量名在语义逻辑都变化了,还不改,程序执行和变量名的含义甚至都可以相反。
很多开发者,为了简化代码的长度,过度压缩,干掉了很多前缀修饰,让具体的代码却带上更宽泛的名词,抽象程度不匹配,显得夸大其词。
每次开发完一个小模块后,都需要进行一次整体的「诚恳性审查」,检查一下代码所做的跟它所说的是否契合。
这种检查看似麻烦,并且在一开始确实麻烦,但很值得做。这种麻烦不是恒定的,随着我们越发熟稔,我们命名技巧愈加成熟,需要的审查时间会缩短。并且,常常可以在审查阶段发展新的修改空间。
总结
我们还可以列举更多要素和技巧,但没有必要了,因为上面其实都是些战术性质的内容。
如果能在战略心态上做出调整,开发者自己可以在实践中总结出上面的内容。而如果战略心态没有改变,上述战术要素只会困扰我们,成为另一个「太抽象了」、「脱离实际」、「太理想化」了的事物。
感受不到这些战术的真正价值和含义。
那么,这个战略心态究竟是什么呢?
是我们对待代码的根本态度,我们是把写代码看作完成目标的工具,还是像写文章甚至写诗一样自身就是目的的活动?
我们会为了写出好的文章和诗歌,斟酌字句,反复修改,我们有为代码做一样的努力吗?
我们把自己的文章和,当作是宝贵的个人创作和资产。我们有用同样的资产角度看待我们的代码吗?
我们在不断提取出子函数的过程中,是感到烦躁,还是快乐?是感到麻烦,还是觉得又增加了优质代码资产,又增加了对问题的深入认识?
很多开发者常常说,代码是解决问题的工具,不要狭隘地只用技术角度思考问题。我这里却强调,代码自身就是目的,是否太技术导向,不值得效仿?
不,并没有冲突。
人不只有一种目的,很多不同情景下,有不同的目的和手段的关系。
代码依然是解决业务问题的工具,但想写出好的代码,在写代码时就应该把好代码当作目的,这种心理完全不影响代码在解决问题层面的工具属性。
这一层面的目的,可以是另一层面的工具,彼此相容。
永远把技术和代码当作工具的开发者,最终技术上成就可能会阻塞,不把代码和技术当回事儿,投入不够,容易遇到技术天花板。
而永远把技术和代码当作目的的开发者,最终非技术领域的能力也会受到影响。
在对待代码的态度上,我们也需要分层处理,前面说的分层处理,是在人类认知心理学层面的,不只是适用于写代码。
关键是要提高我们的认知水平,写代码的水平是认知水平的一个应用。很多时候,不是编程技巧不足阻碍了我们写出优雅的代码,是对问题的认知水平不足。
前面介绍了通过分配抽象的占位变量的技巧,就是围绕认知水平,去解除编程阻塞,推进对问题的处理,积累对问题的理解,提高认知后然后再回来还变量名。
优雅的变量名,不只是来自编程技巧,更多来自领域知识。
认知态度和水平是战略的,编程技巧和经验是战术的。
随着战略水平的增加,我们花费在阅读、沟通和思考的时间会增加,花在编码的时间比例会减少,但质量和效率会增加。
代码效率的增加,一方面来自对领域问题的认识,另一方面也来自掌握了前面提到的高阶抽象后(框架的框架的框架……),我们可以不用真的写代码,也能构建编程模型。
因为我们掌握了编程里的技术概念之间的关系,我们知道有了 x 可以解决 X,有了 y 就一定有 z,而有了 a 操作和 b 元素,可以构成 c 结构,表达 d 意图,放到 e 模块后,能作为 f 层的 g 功能来用……
大量的专业训练,让我们对那些技术领域的抽象事物的关系,就像娱乐圈粉丝们熟悉偶像们之间的八卦关系一样信手拈来。
变量命名问题,不是变量命名的问题,是认知水平的问题,是专业训练程度的问题。
每一次我们认真打磨代码,学习更抽象的编程和数学知识的过程,就是在进行专业训练,为下次可以更好地加工抽象概念做准备。优雅的变量名,是它的副产品。
从改变对代码的态度开始,尝试像写诗一样写代码。
欲穷千里目,更上一层楼