为何我们的代码难以阅读
任何程序员都能写出机器可以阅读的代码,但只有好的程序员才能写出人可以阅读的代码。这句话道出了要写出容易阅读的代码的困难。但是这些困难到底是什么,我们应该如何认识它,正是本文想要探索的问题。
词汇和命名
词汇是思考的材料,如果我们的词汇贫乏,我们的思考也必然破碎。优秀的作家和普通人的差别,很大程度是在词汇的丰富程度上面。据说丘吉尔能掌握四万个英语词汇,而一般中国人,能有几千个词汇量已经很少见了。因此我们在用英语来写代码的时候,常常会陷入词汇不够的困难境地。我们在程序代码中,往往看到很多类似icount, var, num这样名字的变量;还有很多叫做manager, controllor的类,这些都是因为我们想不到应该如何命名导致的。除了名词缺乏,我们的动词往往也很缺乏,证明就是:我们的很多函数名都叫Process(), Run(),Poll(),Loop()诸如此类。如果我们把程序源代码看成是一篇文章,那么这篇文章的词汇,就是各种变量和函数的名字。如果我们在命名上困难重重,这篇文章也一定晦涩难懂。
命名上的困难,除了是因为我们英语词汇量太小以外,另外一个原因,是我们对于业务领域的不了解。我们在接到需求后,往往就急着开始所谓设计和开发:我们会说在这里要用一个哈希表来存放数据,那里用一个回调函数来处理异步结果。——如果我们把程序看成仅仅是一些数据和算法组合,那么我们的命名必然也只是局限于这些数据结构、算法的概念上,比如我们常见到xxMap的结构体,还有xxCallback的函数指针。用这样一堆名字构建起来的程序,就好像摩斯电码一样难以理解。尽管在这些看起来都差不多的字符背后,实现的是一个鲜活而独特的业务需求,但是光看字面是完全无法想象出来的。这个问题实际上也很好解决,就是我们在写程序之前,多去了解这个程序所在的应用领域,看看这个应用领域里面到底是有些什么样的词汇。比如一个商业应用中,就会有Bill、Invoice、Deal等等专用词汇,在游戏应用中,有Player 、NPC、Monster这些概念……我们可以在几乎任何时候,都能从这些业务领域中攫取大量的词汇,来替换掉计算机领域中少的可怜的几个词。如果我们真正的把代码中的命名,变成应用领域的词汇,那么这样的代码片段,就是一个描述某种业务领域的文章,如此,可读性就能大大的加强。
命名本身并不影响程序的运行,但是我们也没必要直接写出好像被扰码器处理过一样的代码。如果我们的命名词汇既准确也生动,那么我们的代码一定也是非常容易读懂的。特别是,我们阅读代码的目的常常不是要评估代码的算法,而仅仅是找到某段业务逻辑的位置来进行修改,这样一个和业务逻辑有关联的命名,能让我们快速跳过大量不相干的代码,直接定位到需要修改的地方,这对代码维护是非常有利的。
语法和模型
传说上帝为了惩罚傲慢自大的人类,让人类开始说不同的语言,而因为说着不同语言,人们无法合作,就无法合力建造巴别通天塔去挑战上帝了。这个故事说明了,语言的不同,能造成多么大的沟通障碍。我以自己少的可怜的语言知识,都能发现不同语言之间,一些重要的区别,是如何阻碍人类之间的沟通的。
第一个是语序问题:汉语和英语,语序都是主谓宾结构的,但是日语和韩语,语序则是主宾谓的,当我们说“我爱你”的时候,在日语韩语的语法是“我你爱”。这种语序上的差异,会导致整个表达方式的不同,从而造成严重的沟通障碍。这样看起来好像汉语和英语还挺接近,但是在定语状语的语序上,这两种语言又是不一样的,汉语的定语在修饰对象的前面,而英语即可在前,也常常在后,比如Man in black。
第二个是语言态度的结合问题。我们常常听到韩语有“思密达”的结尾,实际上这个词汇没有实际含义,只是表示尊敬的含义。在日文中也有大量的这种词汇。这是所谓粘连语的特征,但是在汉语中,我们则使用不同的敬语词汇来表达,比如我们称你为“您”,而英语则通过不同的虚词用法如would来表达敬语语气。我们不得不说人类语言真的是非常复杂。
然而我说了一堆各种人类语言的不同,这和计算机源代码有什么关系呢?毕竟我们用的C/JAVA这些计算机语言,这种语言是不存在上面自然语言的差异的啊?——其实这是有非常重要的关系的,因为我们希望源代码是容易理解的,往往是以自然语言作为标志物的。我们都在尽量的想把源代码写成自然语言的文章,但是如果我们的这个努力目标,自然语言是不一样的,那么我们的努力方向都可能是错误的。因此,如果我们是想让中国程序员更容易看懂,我们可能要尽量维持命名的定语在前,我们可能要把参数列表定义成主、宾的次序。
各种自然语言的不同之处除了上面所说的,还有一个重要的差异,是在于词汇的分布不同。英语中的名词比汉语要多的多,因此才会出现所谓中国的“羊”年,在英语中都不知道应该如何翻译的情况,因为英语中绵羊、山羊都是不同的词。而汉语中的动词,则比英语的多,英语有大量的动词含义是通过动词短语来表达的,比如give in/give up,汉语就是完全不同的“让步”和“放弃”。这个差别体现在编程上是非常关键的,我们知道,面向对象编程,需要以对象类型来对业务代码建模,而由于汉语名词的缺乏,我们常常表达一个对象时,找不到一个专有名词,而是用“做什么什么的东西”来表达这个对象,这对我们代码的设计造成极大的困扰。因为行为的特征在对象上往往是不够稳定的,一旦我们以行为作对象的名字,而这个对象在后续的迭代中被修改行为,就很容易出现名不副实的情况。因此我们在设计面向对象代码的时候,还真的不能仅仅以汉语的习惯去设计,而是要多找找有没有专门表达这个对象的英语名词。
重复和耦合
我们如果想写出如同自然语言一样易读的软件代码,那么我们就一定要以自然语言写文章的结构。但是很可惜的是,自然语言的文章以传情达意为目的,而软件代码主要是控制电脑工作的任务列表。这两者之间有一个重要的差异,就在于“语句”的存在形式上。
我们知道,代码是一行行执行的,而电脑对于数据的处理,往往存在很多类似的、重复的处理步骤。而我们写一篇文章,肯定不会让某一个描述过的句子,反反复复的出现在所有需要的地方。我们会用一些概念词汇来代表这些重复提到的概念,我们会有很多专属名词和缩写,比如“婚戒”“爆炒”这类稍微复杂一些的词汇。这种词汇和说法,能让文章变得简单清晰,突出重点。
其实我们的源代码也可以做到这点,基本的做法就是“封装”:我们把类似的、重复的代码封装成子函数;我们用继承的方法来构建相似的数据对象。如果我们还能用恰如其分的名字来命名这些子函数和子类型,那么我们的代码就能避免长篇累牍的重复代码,从而能让我们更容易的理解。也许,这种封装是一种“额外”的劳动,因为CPU可不管这些,如果你只是想算出结果来,那么完全可以一个函数从头写到尾。但是如果你有意识的做一些有具体业务含义的封装,你会得到另外一个好处,就是代码能更方便的重用。代码重用的首要条件是代码可理解,封装正是对复杂的实现过程的屏蔽,从而让人可以快速理解。而业务领域的重复逻辑是非常常见的,如果代码刚好是一些典型的业务流程,那么这些对应流程的代码,就一定能被重用到大量的类似业务流程的处理那里,这样的好处不言而喻。
总结
这篇文章并没有很深入的去描述,如何从技术角度编写出可读的代码,而主要是关注软件代码和自然语言的差异和联系。因为自然语言本身是我们理解世界的基本工具,所以我们的软件代码,也应该要针对自然语言的特点去设计,才能满足我们人类对代码的理解需求。
感谢大家的阅读,如觉得此文对你有那么一丁点的作用,麻烦动动手指转发或分享至朋友圈。如有不同意见,欢迎后台留言探讨。