编写可读代码(二) 如何命名

来源:互联网 发布:nginx 后端监控 编辑:程序博客网 时间:2024/04/27 20:52

记得看到过一个调查,说程序员最头疼的事情是什么,最后票数最高的是Naming things(http://kb.cnblogs.com/page/192017/)。从中不难看出,命名这件往往被初学者忽略的事情,其实是编写代码中非常非常重要的一环。一个良好的命名,能使自己在写代码过程中的思路更加清晰,可以省却很多不必要的注释,也可以让代码变得更加易读。

下面就我个人的一些体验以及阅读《编写可读代码的艺术》(以下缩略为《编》)和部分《Clean Code》(以下缩略为《CC》)之后的感想,做一个如何命名方面的总结。

一、词语的选择

在我们对变量进行命名的时候,首先要做的一件事就是选择用什么样的单词或者字母的组合来进行命名,在这个过程中有几个要注意的点。

1.慎用小写l以及字母o

当这两个字母不是一个单词的一部分的时候(单独使用应该绝对避免),使用这两个字母要慎重,因为它们和1、0太过接近而导致人们识别的时候很容易发生误解。比方说一个判断语句if(x<l),很容易被人误解为x小于一的判断。因此单独使用是绝对要避免的。而作为非单词的一部分的时候也不建议使用。

2.使用专业的词汇

使用更符合计算机专业领域的词语,使得代码阅读者可以迅速领会其中的含义,而不必通过上下文去推测。

比方说一个线程类中有一个方法叫stop(),这个方法并不是一个非常好的名字,因为我们不知道这是不是一个可以恢复的停止,如果是可以恢复的,命名为pause会更合适,如果是不可以恢复的,命名为terminate则更好。在Qt的QThread中,有quit,terminate两个槽,Qt使用了不同的两个单词来表现其中的差异。quit是正常的退出线程,会等待线程完成当前工作,而terminate则是立即结束线程(取决于操作系统),当前线程正在操作的数据可能会有丢失的风险。

3.使用具体的词汇,而非抽象

命名的时候要清晰的表达出其内在的含义,而不是含糊的说大概是什么、大概做什么,读者为了理解一个含糊的名字,不得不大量阅读上下文才能很好的理解,但依然可能会出现误解。

《编》中的一个例子是,假设有一个方法,检测服务器是否可以监听某个端口,如果命名为ServerCanStart就很抽象,而命名为CanListenOnPort则比较具体。

另外要说的就是不要用一些看起来觉得可爱,但实际意义不明的抽象名字。比方说有一个数字变量命名为HolyNumber就是一个意义不明让人不停猜测的名字,假设这个名字的本意是用来检验文件是否正确,那么CheckSum(如果是采用校验和的方式)、MagicNumber(如果是固定标识数字)可能是一个更好的名字。

《CC》中的一个例子是,有一个方法命名为HolyHandGrenade,看了之后大概觉得很有趣,但完全不明这个方法是做什么的。但如果命名为DeleteItems,我们则能马上知道其用途了。

4.词性的选择

选择合适的词性,可以帮我们更好的领会我们命名的事物的类型及用途,使用这种方法比添加前缀后缀的方法要有效的多。下面举几个例子:

类的名称 使用名词或者名词短语

方法的名称 使用动词或者动词短语

布尔变量  使用形容词或形容词短语

二、包含更多的信息

《CC》:一个好的名字要考虑它为什么存在,它是用来做什么的以及怎样使用它。如果一个名称需要用注释来加以解释,那么基本上就没有起一个好的名字。

仔细读上面的这句话,可以对我们命名会有很大的帮助。

包含更多的信息主要包含两部分:

1.用更多的单词(包括大家熟知的缩写)来进行描述

在写代码的时候,很常见的一种注释就是标注时间函数输入时间输出时间的单位。根据上面的那句话,注释基本上就说明了这个名字起的不好。所以,我们对这类函数、变量起名的时候,可以考虑将单位信息包含进去。比方说GetTime_ms,但这种命名方式不美观也不直观,更直观的命名方式是GetMilliSecond,在不会引起误解的情况下也可以使用GetmSecond。另外,还有另外一种方式把这些信息包含进来,如建立一个新的类(或者有限的可以使用枚举方式,如月份、星期),充分利用类型的作用,不但可以包括更多的信息,还可以利用编译器的类型检查,及时发现类型不匹配的错误。

如SetDate(2014/*Year*/,10/*Month*/,14/*Day*/);

变更为:

class Year; enum Month{}; class Day;

SetDate(Year(2014),October,Day(14));

还有一些常见的情况就是我们要对方法或者变量进行一些描述,这时候我们要挑选一些精确的词语来命名,达到包含更多信息的目的。

2.去除不包含有效信息的单词、替换描述不精确的单词

比方说一个方法的名字是HowLongToCalculateTheResult,HowLong用一个Time代替即可,To、The都属于不包含有效信息的单词,而result也没有包含什么有效信息,如果是多个结果,则应该具体指明是什么的结果,如果只是一个结果,则完全可以去掉。因此CalculateTime是一个比较合适的名字。

三、没有二义性

二义性或者说多义性是命名中遇到最让人感到头痛的问题。下面就其两个方面来进行说明。

1.有两种(或以上)不同含义的名字

假设有一个方法的名字叫做OrderBill,这是一个典型的要让人花费精力阅读上下文来了解其真正含义的名字,因为单从名字上来看,我们不知道它是要整理排序订单,还是下订单,Order的二义性使得这是一个差劲的名字。也许SortBill和SubmitBill是一个不错的选择。

2.容易让人误解、迷惑的名字

《编》中有一个典型的例子。

results=Database.all_objects.filter("year <=2011")

对于这行代码,到底该怎么理解呢?结果是“年份小于等于2011的对象”还是“年份不小于等于2011的对象”?遇到这种代码,读者不得不去翻看文档,了解filter的工作机制,才能读懂这段代码。所以这是一个差劲的名字。

除此之外,书中的其他的几个例子我也觉得非常好。

关于极限:

我们对一个变量进行限制,让其不超过某个数值,比方说10,我们常用的一种方法是定义一个常量或一个宏,如XXX_BIG_LIMIT=10。但就在这个边界处很容易产生歧义,这个limit的值到底是否可以等于总是让人疑惑。如果命名为XXX_MAX,则我们很容易理解这个界限值是被包含的。min也是同理。

关于范围:

比方说我们遍历一个数组,如果用start、stop来进行遍历,而容易出现的疑问就是stop到底是数组的最后一个元素还是最后一个元素的下一个位置。为了避免这种情况,在表示包含的时候,用first、last是很好的一个选择。表示不包含的时候(结尾不包含),begin、end更加。说起来,其实end也是有二义性的,但我们已经找不到更好的单词了,并且这种使用方式已经很普遍(如C++标准库),所以可以默认。

关于大小:

size和length是很常用的两个表示大小的名字。但有的时候,这两个并不是一种很好的选择。因为我们可以有很多解读的方式,比方说一个容器内元素的个数,一个容器占用空间的字节数等。不过对于已经广泛使用的指定方式,我们也是可以使用的,但千万不能使用大家默认外的含义。

在Qt中,以QVector为例,表示大小的函数有三个,size()、length()、count(),其实这三个函数的结果都是容器中的元素的个数。大概是由于历史问题和兼容问题,Qt提供了三个不同名字却同样功能的函数。但我们在使用的时候要尽量保证一致性,如使用size则全部使用size,否则当读者同时看到size、length时就会想到,是不是两者有着不同的含义从而浪费时间。

关于布尔值:

布尔值命名不当,非常容易产生二义性。如readPassword=true,我们可以解释为我们需要读取密码,也可以解释为已经读取了密码。对于此例,命名为needPassword和userIsAuthenticated更好。

另外,布尔变量不要命名为反义的名字,会给读者带来很大的障碍。如disableFunction=false,读者需要做额外不必要的思考才能理解其含义,是不必要的。

四、可搜索性和即时维护

可搜索性是人们在写代码时容易忽略的一点。

我们在写代码的时候经常喜欢使用很短小的名字,这样带来的后果可能就是随着项目的变大,这个短小的名字成为了某个名字的前缀或者一部分,其直接结果就是当我们要检索这个名字出现的位置的时候会搜索到很多不相干的结果,加大了我们维护代码的成本。因此,起名字的时候考虑到名字的可检索性是增强代码可维护性很重要的一部分。

由于项目不停开发的过程中,需求的变化不可避免,所以经常会出现以前一个很恰当的名字不能准确描述当前的状况了。这个时候我们要马上对代码进行处理,利用前面保证的可检索性,迅速对不准确的名字进行维护。同时也可以进一步保证名字的可检索性。

比方说一个函数connectToLocalDatabase,后来我们添加功能,使其也可以连接网络上的数据库了,这时我们也许要对其进行更名为connectToDatabase。但也许对函数进行拆分是更好的选择。

五、名字的长度

名字的长度是命名中非常重要的一个环节。名字太长,会加大读者理解的时间,反而增加了阅读障碍,名字太短,包含的信息太少,读者又难以理解其含义。那么什么样长度的名字是合适的呢?

总的来说,名字的长度是与作用范围正相关的。最典型的例子就是在一个只有几行的代码里,我们可以用非常短的名字而不引起任何阅读障碍。如:

map<string,int> m;

LookUpNamesNumbers(&m);

Print(m);

这三行代码放在一起,虽然名字只是m一个字母,我们也能无障碍的阅读。但如果第一行定义在全局,那么直看到后两行代码,我们就很难知道这个m到底是什么了,这时候我们就需要长一些,包含更多信息的名字了。

另外,i、j、k是大家通用的循环变量,一般不会引起什么歧义,但循环部分的代码行数最好不要太多,否则就需要一些其他的措施来加大代码的可读性了。如对循环变量更换名字,拆分函数,调整程序逻辑等。

最后要说的一点也是我最最讨厌别人做的一件事就是乱用缩略的问题。很多人为了缩短名字的长度,生生造出了各种各样的缩略,殊不知这样反而产生了更大的阅读障碍。比方说一个名字LookUpNames缩略为LUNames,会让人完全不知所云。

六、代码中的常数处理

代码中经常会出现各种各样的常数量,对于这种常数量我们一定要根据其作用范围起一个合适的名字,使用枚举、常量、宏等方式来进行定义,这样后期代码才具备可维护性。比方说代码中有两处都出现了常数10,但这两个常数10分别代表不同的含义,这时候如果是直接写上去数字的话,即使我们利用检索来修改也会变得非常棘手。

另外,有人会建议加k前缀的方式命名常量(而不是全大写)来区分常量和宏。嗯,是否采用要看实际环境、作用范围,以及,见仁见智了。

七、与命名规范的探讨

一般来说,考虑命名的时候是一定要遵循团队事先制定好的命名规范的。但有的时候,命名规范中的有些规定也并不是最佳的选择。

匈牙利命名法大概是最受诟病的命名方法了。首先,它包含了太多种类的前缀,学习记忆这些前缀会给读者带来很大的额外的负担。另外,它有着很多不同的变种。不同的人写出来的名字很可能并不一致(即使他们都宣称使用匈牙利命名法)。最后,太多的额外信息(大多都是编程细节信息)会严重干扰我们对真正含义部分单词的理解,大幅提高阅读代码的难度。想起来我当时想学习一下Windows API编程的时候就是被那可怕的匈牙利命名法吓跑的,最后投入了Qt的怀抱。

另外还有一个就是对于成员变量或者私有变量添加前缀或后缀问题。有些人会建议添加m_前缀或者_后缀,用以辨识是否是成员变量。但我没有这个习惯,在需要区分是否是成员变量的时候我更喜欢利用this指针来区分是否是成员变量。比方说下面的例子:

SetThreshold(float threshold){this->threshold=threshold;}

另外一个不建议添加前后缀的原因就是,当人们在长期阅读代码时发现前缀后缀更多代表的是编程细节信息,而无关于理解程序的时候,容易忽略命名的前缀或后缀,而只关注有意义的部分,最后造成这些前缀失去其应有的意义,还会带来额外的负担。

0 0
原创粉丝点击