程序员之间的互相尊重体现在他所写的代码中。他们对工作的尊重也体现在那里。
在《Clean Code》一书中Bob大叔认为在代码阅读过程中人们说脏话的频率是衡量代码质量的唯一标准。这也是同样的道理。
这样,代码最重要的读者就不再是编译器、解释器或者电脑了,而是人。写出的代码能让人快速理解、轻松维护、容易扩展的程序员才是专业的程序员。
代码应当易于理解
可读性基本定理:代码的写法应当使别人理解它所需的时间最小化。
本书的余下部分将讨论如何把“易读”这条原则应用在不同的场景中。但是请记住,当你犹豫不决时,可读性基本定理总是先于本书中任何其他条例或原则。
把信息装到名字里
我们在程序中见到的很多名字都很模糊,例如tmp。就算是看上去合理的词,如size或者get,也都没有装入很多信息。本章会告诉你如何把信息装入名字中。
选择专业的词
“把信息装入名字中”包括要选择非常专业的词,并且避免使用“空洞”的词。
例如,“get”这个词就非常不专业,例如在下面的例子中:
def GetPage(url): ...
“get”这个词没有表达出很多信息。这个方法是从本地的缓存中得到一个页面,还是从数据库中,或者从互联网中?如果是从互联网中,更专业的名字可以是FetchPage()或者Download-Page()。
下面是一个BinaryTree类的例子:
class BinaryTree
{
int Size();
...
};
你期望Size()方法返回什么呢?树的高度,节点数,还是树在内存中所占的空间?
问题是Size()没有承载很多信息。更专业的词可以是Height()、NumNodes()或者MemoryBytes()。
另外一个例子,假设你有某种Thread类:
class Thread
{
void Stop();
...
}
Stop()这个名字还可以,但根据它到底做什么,可能会有更专业的名字。例如,你可以叫它Kill(),如果这是一个重量级操作,不能恢复。或者你可以叫它Pause(),如果有方法让它Re-sume()。
找到更有表现力的词
要勇于使用同义词典或者问朋友更好的名字建议。英语是一门丰富的语言,有很多词可以选择。
下面是一些例子,这些单词更有表现力,可能适合你的语境:
但别得意忘形。在PHP中,有一个函数可以explode()一个字符串。这是个很有表现力的名字,描绘了一幅把东西拆成碎片的景象。但这与split()有什么不同?(这是两个不一样的函数,但很难通过它们的名字来猜出不同点在哪里。)
清晰和精确比装可爱好。
避免像 tmp 和 retval 这样泛泛的名字
使用像tmp、retval和foo这样的名字往往是“我想不出名字”的托辞。与其使用这样空洞的名字,不如挑一个能描述这个实体的值或者目的的名字。
例如,下面的JavaScript函数使用了retval:
var euclidean_norm = function(v) {
var retval = 0.0;
for (var i = 0; i < v.length; i += 1)
retval += v[i] * v[i];
return Math.sqrt(retval);
};
当你想不出更好的名字来命名返回值时,很容易想到使用retval。但retval除了“我是一个返回值”外并没有包含更多信息(这里的意义往往也是很明显的)。
好的名字应当描述变量的目的或者它所承载的值。在本例中,这个变量正在累加v的平方。因此更贴切的名字可以是sum_squares。这样就提前声明了这个变量的目的,并且可能会帮忙找到缺陷。
例如,想象如果循环的内部被意外写成:
retval += v[i];
如果名字换成sum_squares这个缺陷就会更明显:
sum_squares += v[i]; //我们要累加的"square"在哪里?缺陷!
retval这个名字没有包含很多信息。用一个描述该变量的值的名字来代替它。
如果你要使用像tmp、it或者retval这样空泛的名字,那么你要有个好的理由。
用具体的名字代替抽象的名字
在给变量、函数或者其他元素命名时,要把它描述得更具体而不是更抽象。
例如,假设你有一个内部方法叫做ServerCanStart(),它检测服务是否可以监听某个给定的TCP/IP端口。然而Server-CanStart()有点抽象。CanListenOnPort()就更具体一些。这个名字直接地描述了这个方法要做什么事情。
为名字附带更多信息
我们前面提到,一个变量名就像是一个小小的注释。尽管空间不是很大,但不管你在名中挤进任何额外的信息,每次有人看到这个变量名时都会同时看到这些信息。
因此,如果关于一个变量有什么重要事情的读者必须知道,那么是值得把额外的“词”添加到名字中的。例如,假设你有一个变量包含一个十六进制字符串:
string id; // Example: "af84ef845cd8"
如果让读者记住这个ID的格式很重要的话,你可以把它改名为hex_id。
带单位的值
如果你的变量是一个度量的话(如时间长度或者字节数),那么最好把名字带上它的单位。例如,这里有些JavaScript代码用来度量一个网页的加载时间:
var start = (new Date()).getTime(); // top of the page
...
var elapsed = (new Date()).getTime() - start; // bottom of the page
document.writeln("Load time was: " + elapsed + " seconds");
这段代码里没有明显的错误,但它不能正常运行,因为get-Time()会返回毫秒而非秒。通过给变量结尾追加_ms,我们可以让所有的地方更明确:
var start_ms = (new Date()).getTime(); // top of the page...
var elapsed_ms = (new Date()).getTime() - start_ms; // bottom of the page
document.writeln("Load time was: " + elapsed_ms / 1000 + " seconds");
除了时间,还有很多在编程时会遇到的单位。下表列出一些没有单位的函数参数以及带单位的版本:
函数参数 | 带单位的参数 |
---|---|
Start(int delay) | delay → delay_secs |
CreateCache(int size) | size → size_mb |
ThrottleDownload(float limit) | limit → max_kbps |
Rotate(float angle) | angle → degrees_cw |
附带其他重要属性
这种给名字附带额外信息的技巧不仅限于单位。在对于这个变量存在危险或者意外的任何时候你都该采用它。
例如,很多安全漏洞来源于没有意识到你的程序接收到的某些数据还没有处于安全状态。在这种情况下,你可能想要使用像 untrustedUrl 或者 unsafeMessageBody 这样的名字。在调用了清查不安全输入的函数后,得到的变量可以命名为 trustedUrl 或者 safeMessageBody 。
但你不应该给程序中每个变量都加上像 unescaped_ 或者 _utf8 这样的属性。如果有人误解了这个变量就很容易产生缺陷,尤其是会产生像安全缺陷这样可怕的结果,在这些地方这种技巧最有用武之地。基本上,如果这是一个需要理解的关键信息,那就把它放在名字里。
名字应该有多长
在小的作用域里可以使用短的名字
作用域
小的标识符(对于多少行其他代码可见)也不用带上太多信息。也就是说,因为所有的信息(变量的类型、它的初值、如何析构等)都很容易看到,所以可以用很短的名字。如果一个标识符有较大的作用域,那么它的名字就要包含足够的信息以便含义更清楚。
首字母缩略词和缩写
所以经验原则是:团队的新成员是否能理解这个名字的含义?如果能,那可能就没有问题。例如,对程序员来讲,使用eval来代替evaluation,用doc来代替document,用str来代替string是相当普遍的。因此如果团队的新成员看到FormatStr()可能会理解它是什么意思,然而,理解BEManager可能有点困难。
丢掉没用的词
有时名字中的某些单词可以拿掉而不会损失任何信息。例如,Convert To String()就不如To String()这个更短的名字,而且没有丢失任何有用的信息。同样,不用DoServeLoop(),ServeLoop()也一样清楚。
利用名字的格式来传递含义
对于下划线、连字符和大小写的使用方式也可以把更多信息装到名字中。对不同的实体使用不同的格式就像语法高亮显示的形式一样,能帮你更容易地阅读代码。
不会误解的名字
要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”要仔细审视这个名字。
Filter()
假设你在写一段操作数据库结果的代码:
results = Database.all_objects.filter("year <= 2011")
结果现在包含哪些信息?
- 年份小于或等于2011的对象?
- 年份不小于或等于2011年的对象?
这里的问题是“filter”是个二义性单词。我们不清楚它的含义到底是“挑出”还是“减掉”。最好避免使用“filter”这个名字,因为它太容易误解。
Clip(text, length)
假设你有个函数用来剪切一个段落的内容:
# Cuts off the end of the text, and appends "..."
def Clip(text, length):
...
你可能会想象到Clip()的两种行为方式:
- 从尾部删除length的长度
- 截掉最大长度为length的一段
第二种方式(截掉)的可能性最大,但还是不能肯定。与其让读者乱猜代码,还不如把函数的名字改成Truncate(text,length)
。
然而,参数名length也不太好。如果叫max_length的话可能会更清楚。这样也还没有完。就算是max_length这个名字也还是会有多种解读:
- 字节数
- 字符数
- 字数
如你在前一章中所见,这属于应当把单位附加在名字后面的那种情况。在本例中,我们是指“字符数”,所以不应该用max_length,而要用max_chars。
推荐用min和max来表示(包含)极限
命名极限最清楚的方式是在要限制的东西前加上max_或者min_。
推荐用first和last来表示包含的范围
下面是另一个例子,你没法判断它是“少于”还是“少于且包含”:
print integer_range(start=2, stop=4)
# Does this print [2,3] or [2,3,4] (or something else)?
尽管start是个合理的参数名,但stop可以有多种解读。对于这样包含的范围(这种范围包含开头和结尾),一个好的选择是first/last。
例如:
set.PrintKeys(first="Bart", last="Maggie")
不像stop,last这个名字明显是包含的。除了first/last,min/max这两个名字也适用于包含的范围,如果它们在上下文中“听上去合理”的话。
推荐用begin和end来表示包含/排除范围
对于命名包含/排除范围典型的编程规范是使用begin/end。
但是end这个词有点二义性。例如,在句子“我读到这本书的end部分了”,这里的end是包含的。遗憾的是,英语中没有一个合适的词来表示“刚好超过最后一个值”。
因为对begin/end的使用是如此常见(至少在C++标准库中是这样用的,还有大多数需要“分片”的数组也是这样用的),它已经是最好的选择了。
给布尔值命名
通常来讲,加上像is、has、can或should这样的词,可以把布尔值变得更明确。
例如,SpaceLeft() 函数听上去像是会返回一个数字,如果它的本意是返回一个布尔值,可能 HasSapceLeft() 个这名字更好一些。
最后,最好避免使用反义名字。
例如,不要用:bool disable_ssl = false;
而更简单易读(而且更紧凑)的表示方式是:bool use_ssl = true;
与使用者的期望相匹配
有些名字之所以会让人误解是因为用户对它们的含义有先入为主的印象,就算你的本意并非如此。在这种情况下,最好放弃这个名字而改用一个不会让人误解的名字。
get*()
很多程序员都习惯了把以get开始的方法当做轻量级访问器
这样的用法,它只是简单地返回一个内部成员变量。如果违背这个习惯很可能会误导用户。
以下是一个用Java写的例子,请不要这样做:
public class StatisticsCollector {
public void addSample(double x) {}
public double getMean() {
// Iterate through all samples and return total / num_samples
}
}
在这个例子中,getMean()的实现是要遍历所有经过的数据并同时计算中值。如果有大量的数据的话,这样的一步可能会有很大的代价!但一个容易轻信的程序员可能会随意地调用get-Mean(),还以为这是个没什么代价的调用。
相反,这个方法应当重命名为像computeMean()这样的名字,后者听起来更像是有些代价的操作。(另一种做法是,用新的实现方法使它真的成为一个轻量级的操作。)
list::size()
下面是一个来自C++标准库中的例子。曾经有个很难发现的缺陷,使得我们的一台服务器慢得像蜗牛在爬,就是下面的代码造成的:
void ShrinkList( list<Node> & list, int max_size )
{
while ( list.size() > max_size )
{
FreeNode( list.back() );
list.pop_back();
}
}
这里的“缺陷”是,作者不知道list.size()是一个O(n)操作——它要一个节点一个节点地历数列表,而不是只返回一个事先算好的个数,这就使得ShrinkList()成了一个O(n2)操作。
这段代码从技术上来讲“正确”,事实上它也通过了所有的单元测试。但当把ShrinkList()应用于有100万个元素的列表上时,要花超过一个小时来完成!
可能你在想:“这是调用者的错,他应该更仔细地读文档。”有道理,但在本例中,list.size()不是一个固定时间的操作,这一点是出人意料的。所有其他的C++容器类的size()方法都是时间固定的。
假使 size() 的名字是 countSize() 或者 countElements() ,很可能就会避免相同的错误。C++标准库的作者可能是希望把它命名为 size() 以和所有其他的容器一致,就像 vector 和 map 。但是正因为他们的这个选择使得程序员很容易误把它当成一个快速的操作,就像其他的容器一样。谢天谢地,现在最新的C++标准库把size()改成了O(1)。
总结
不会误解的名字是最好的名字——阅读你代码的人应该理解你的本意,并且不会有其他的理解。遗憾的是,很多英语单词在用来编程时是多义性的,例如 filter、length和limit。
在你决定使用一个名字以前,要吹毛求疵一点,来想象一下你的名字会被误解成什么。最好的名字是不会误解的。
- 当要定义一个值的上限或下限时,max_和min_是很好的前缀。
- 对于包含的范围,first和last是好的选择。
- 对于包含/排除范围,begin和end是最好的选择,因为它们最常用。
- 当为布尔值命名时,使用is和has这样的词来明确表示它是个布尔值,避免使用反义的词(例如disable_ssl)。
- 要小心用户对特定词的期望。例如,用户会期望get()或者size()是轻量的方法。
审美
好的源代码应当“看上去养眼”。本章会告诉大家如何使用好的留白、对齐及顺序来让你的代码变得更易读。确切地说,有三条原则:
- 使用一致的布局,让读者很快就习惯这种风格。
- 让相似的代码看上去相似。
- 把相关的代码行分组,形成代码块。
审美与设计
在这里中,我们只关注可以改进代码的简单 审美
方法。这些类型的改变很简单并且常常能大幅地提高可读性。有时大规模地重构代码(例如拆分出新的函数或者类)可能会更有帮助。我们的观点是好的审美与好的设计是两种独立的思想。最好是同时在两个方向上努力做到更好。
用方法来整理不规则的东西
使代码“看上去漂亮”通常会带来不限于表面层次的改进,它可能会帮你把代码的结构做得更好。
选一个有意义的顺序,始终一致地使用它
如: React生命周期的顺序。
把声明按块组织起来
把代码分成“段落”
个人风格与一致性
一致的风格比“正确”的风格更重要。
该写什么样的注释
注释的目的是尽量帮助读者了解得和作者一样多。
什么不需要注释
不要为那些从代码本身就能快速推断的事实写注释。
不要为了注释而注释
不要给不好的名字加注释——应该把名字改好
一个好的名字比一个好的注释更重要,因为在任何用到这个函数的地方都能看得到它。
通常来讲,你不需要“拐杖式注释”——试图粉饰可读性差的代码的注释。写代码的人常常把这条规则表述成:好代码>坏代码+好注释。
记录你的思想
很多好的注释仅通过“记录你的想法”就能得到,也就是那些你在写代码时有过的重要想法。
加入“导演评论”
电影中常有“导演评论”部分,电影制作者在其中给出自己的见解并且通过讲故事来帮助你理解这部电影是如何制作的。同样,你应该在代码中也加入注释来记录你对代码有价值的见解。
为代码中的瑕疵写注释
代码始终在演进,并且在这过程中肯定会有瑕疵。不要不好意思把这些瑕疵记录下来。
例如,当代码需要改进时:
// TODO: 采用更快算法
或者当代码没有完成时:
// TODO(dustin):处理除JPEG以外的图像格式
有几种标记在程序员中很流行:
- 标记通常的意义TODO:我还没有处理的事情
- FIXME:已知的无法运行的代码
- HACK:对一个问题不得不采用的比较粗糙的解决方案
- XXX:危险!这里有重要的问题
重要的是你应该可以随时把代码将来应该如何改动的想法用注释记录下来。这种注释给读者带来对代码质量和当前状态的宝贵见解,甚至可能会给他们指出如何改进代码的方向。
给常量加注释
当定义常量时,通常在常量背后都有一个关于它是什么或者为什么它是这个值的“故事”。
有些常量不需要注释,因为它们的名字本身已经很清楚(例如SECONDS_PER_DAY)。但是在我们的经验中,很多常量可以通过加注释得以改进。这不过是匆匆记下你在决定这个常量值时的想法而已。
站在读者的角度
- 预料到代码中哪些部分会让读者说:“啊?”并且给它们加上注释。
- 为普通读者意料之外的行为加上注释。
- 在文件/类的级别上使用“全局观”注释来解释所有的部分是如何一起工作的。
- 用注释来总结代码块,使读者不致迷失在细节中。
写出言简意赅的注释
- 当像“it”和“this”这样的代词可能指代多个事物时,避免使用它们。
- 尽量精确地描述函数的行为。
- 在注释中用精心挑选的输入/输出例子进行说明。
- 声明代码的高层次意图,而非明显的细节。
- 用嵌入的注释(如Function(/arg =/...))来解释难以理解的函数参数。
- 用含义丰富的词来使注释简洁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。