与其把我们主要的任务想象成指示计算机做什么,不如让我们关注解释给人类我们希望让计算机做什么。
程序员之间的互相尊重体现在他所写的代码中。他们对工作的尊重也体现在那里。
- 表面层次上的改进
- 命名、注释以及审美——可以用于代码库每一行的小提示。
- 简化循环和逻辑
- 在程序中定义循环、逻辑和变量,从而使得代码更容易理解。
- 重新组织你的代码
- 在更高层次上组织大的代码块以及在功能层次上解决问题的方法。
- 精选话题
- 把”易于理解”的思想应用于测试以及大数据结构代码的例子。
代码应当易于理解
可读性基本定理
- 关键思想
- 代码的写法应当使别人理解它所需的时间最小化。
- 当我们说“理解”时,我们对这个词有个很高的标准。如果有人真的完全理解了你的代码,他就应该能改动它、找出缺陷并且明白它是如何与你代码的其他部分交互的。
- 总是越小越好吗
- 因此尽管减少代码行数是一个好目标,但把理解代码所需的时间最小化是一个更好的目标。
表面层次的改进
我们的可读性之旅从我们认为“表面层次”的改进开始:选择好的名字、写好的注释以及把代码整洁地写成更好的格式。这些改变很容易应用。你可以在“原位”做这些改变而不必重构代码或者改变程序的运行方式。你还可以增量地做这些修改却不需要投入大量的时间。
把信息装到名字里
无论是命名变量、函数还是类,都可以使用很多相同的原则。我们喜欢把名字当做一条小小的注释。尽管空间不算很大,但选择一个好名字可以让它承载很多信息。
- 关键思想
- 把信息装入名字中。
本章分成6个专题:
- 选择专业的词。
- 避免泛泛的名字(或者说要知道什么时候使用它)。
- 用具体的名字代替抽象的名字。
- 使用前缀或后缀来给名字附带更多信息。
- 决定名字的长度。
- 利用名字的格式来表达含义
选择专业的词
“把信息装入名字中”包括要选择非常专业的词,并且避免使用“空洞”的词。
例如,“get”这个词就非常不专业,例如在下面的例子中:
def GetPage(url): ...
“get”这个词没有表达出很多信息。这个方法是从本地的缓存中得到一个页面,还是从数据库中,或者从互联网中?如果是从互联网中,更专业的名字可以是FetchPage()
或者DownloadPage()
。
下面是一个BinaryTree类的例子:
class BinaryTree
{ int Size();
...
};
你期望Size()
方法返回什么呢?树的高度,节点数,还是树在内存中所占的空间?问题是Size()
没有承载很多信息。更专业的词可以是Height()
、NumNodes()
或者MemoryBytes()
。
另外一个例子,假设你有某种Thread类:
class Thread
{ void Stop();
...
};
Stop()
这个名字还可以,但根据它到底做什么,可能会有更专业的名字。例如,你可以叫它Kill()
,如果这是一个重量级操作,不能恢复。或者你可以叫它Pause()
,如果有方法让它Resume()
。
- send
- deliver、dispatch、announce、distribute、route
- find
- search、extract、locate、recover
- start
- launch、create、begin、open
- make
- create、set up、build、generate、compose、add、new
关键思想:清晰和精确比装可爱好。
- 避免像tmp和retval这样泛泛的名字
- 使用像tmp、retval和foo这样的名字往往是“我想不出名字”的托辞。与其使用这样空洞的名字,不如挑一个能描述这个实体的值或者目的的名字。
- retval这个名字没有包含很多信息。用一个描述该变量的值的名字来代替它。
- tmp这个名字只应用于短期存在且临时性为其主要存在因素的变量。
- 如果你要使用像tmp、it或者retval这样空泛的名字,那么你要有个好的理由。
- 为名字附带更多信息
- 如果你的变量是一个度量的话(如时间长度或者字节数),那么最好把名字带上它的单位。
- 名字应该有多长
- 当选择好名字时,有一个隐含的约束是名字不能太长。
当你去短期度假时,你带的行李通常会比长假少。同样,“作用域”小的标识符(对于多少行其他代码可见)也不用带上太多信息。也就是说,因为所有的信息(变量的类型、它的初值、如何析构等)都很容易看到,所以可以用很短的名字。
总结
本章唯一的主题是:把信息塞入名字中。这句话的含意是,读者仅通过读到名字就可以获得大量信息。
下面是讨论过的几个小提示:
- 使用专业的单词——例如,不用Get,而用Fetch或者Download可能会更好,这由上下文决定。
- 避免空泛的名字,像tmp和retval,除非使用它们有特殊的理由。
- 使用具体的名字来更细致地描述事物——Server Can Start()这个名字就比CanListenOnPort更不清楚。
- 给变量名带上重要的细节——例如,在值为毫秒的变量后面加上
_ms
,或者在还需要转义的,未处理的变量前面加上raw_
。 - 为作用域大的名字采用更长的名字——不要用让人费解的一个或两个字母的名字来命名在几屏之间都可见的变量。对于只存在于几行之间的变量用短一点的名字更好。
- 有目的地使用大小写、下划线等——例如,你可以在类成员和局部变量后面加上”_”来区分它们。
不会误解的名字
关键思想: 要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”要仔细审视这个名字。
例子:Clip(text, length)
- 命名极限最清楚的方式是在要限制的东西前加上
max_
或者min_
。 - 推荐用first和last来表示包含的范围
- 推荐用first和last来表示包含的范围
- 推荐用begin和end来表示包含/排除范围
总结
- 当要定义一个值的上限或下限时,
max_
和min_
是很好的前缀。对于包含的范围,first和last是好的选择。对于包含/排除范围,begin和end是最好的选择,因为它们最常用。 - 当为布尔值命名时,使用is和has这样的词来明确表示它是个布尔值,避免使用反义的词(例如disable_ssl)。
- 要小心用户对特定词的期望。例如,用户会期望get()或者size()是轻量的方法。
审美
确切地说,有三条原则:
- 使用一致的布局,让读者很快就习惯这种风格。
- 让相似的代码看上去相似。
- 把相关的代码行分组,形成代码块。
用方法来整理不规则的东西
使代码“看上去漂亮”通常会带来不限于表面层次的改进,它可能会帮你把代码的结构做得更好。
把代码分成“段落”
书面文字要分成段落是由于以下几个原因:
- 它是一种把相似的想法放在一起并与其他想法分开的方法。
- 它提供了可见的“脚印”,如果没有它,会很容易找不到你读到哪里了。
- 它便于段落之间的导航。
- 因为同样的原因,代码也应当分成“段落”。
个人风格与一致性
关键思想 一致的风格比“正确”的风格更重要。
总结
大家都愿意读有美感的代码。通过把代码用一致的、有意义的方式“格式化”,可以把代码变得更容易读,并且可以读得更快。
下面是讨论过的一些具体技巧:
- 如果多个代码块做相似的事情,尝试让它们有同样的剪影。
- 把代码按“列”对齐可以让代码更容易浏览。
- 如果在一段代码中提到A、B和C,那么不要在另一段中说B、C和A。选择一个有意义的顺序,并始终用这样的顺序。
- 用空行来把大块代码分成逻辑上的“段落”。
该写什么样的注释
关键思想 注释的目的是尽量帮助读者了解得和作者一样多。
我们把本章组织成以下几个部分:
- 了解什么不需要注释。
- 用代码记录你的思想。
- 站在读者的角度,去想象他们需要知道什么。
什么不需要注释
关键思想
- 不要为那些从代码本身就能快速推断的事实写注释。
- 不要为了注释而注释
- 不要给不好的名字加注释——应该把名字改好
通常来讲,你不需要“拐杖式注释”——试图粉饰可读性差的代码的注释。写代码的人常常把这条规则表述成:好代码>坏代码+好注释。
记录你的思想
代码始终在演进,并且在这过程中肯定会有瑕疵。不要不好意思把这些瑕疵记录下来。
标记 通常的意义
- TODO:我还没有处理的事情
- FIXME:已知的无法运行的代码
- HACK:对一个问题不得不采用的比较粗糙的解决方案
- XXX:危险!这里有重要的问题
有些常量不需要注释,因为它们的名字本身已经很清楚(例如SECONDS_PER_DAY
)。但是在我们的经验中,很多常量可以通过加注释得以改进。这不过是匆匆记下你在决定这个常量值时的想法而已。
站在读者的角度
当为一个函数或者类写文档时,可以问自己这样的问题:“这段代码有什么出人意料的地方?会不会被误用?”基本上就是说你需要“未雨绸缪”,预料到人们使用你的代码时可能会遇到的问题。
对于团队的新成员来讲,最难的事情之一就是理解“全局观”——类之间如何交互,数据如何在整个系统中流动,以及入口点在哪里。设计系统的人经常忘记给这些东西加注释,“只缘身在此山中”。
我们的建议是你可以做任何能帮助读者更容易理解代码的事。这可能也会包含对于“做什么”、“怎么做”或者“为什么”的注释(或者同时注释这三个方面)。
最后的思考——克服“作者心理阻滞”
请注意我们把写注释这件事拆成了几个简单的步骤:
- 不管你心里想什么,先把它写下来。
- 读一下这段注释,看看有没有什么地方可以改进。
- 不断改进。
当你经常写注释,你就会发现步骤1所产生的注释变得越来越好,最后可能不再需要做任何修改了。并且通过早写注释和常写注释,你可以避免在最后要写一大堆注释这种令人不快的状况。
什么地方不需要注释:
- 能从代码本身中迅速地推断的事实。
- 用来粉饰烂代码(例如蹩脚的函数名)的“拐杖式注释”——应该把代码改
你应该记录下来的想法包括:
- 对于为什么代码写成这样而不是那样的内在理由(“指导性批注”)。
- 代码中的缺陷,使用像TODO:或者XXX:这样的标记。
- 常量背后的故事,为什么是这个值。
站在读者的立场上思考:
- 预料到代码中哪些部分会让读者说:“啊?”并且给它们加上注释。
- 为普通读者意料之外的行为加上注释。
- 在文件/类的级别上使用“全局观”注释来解释所有的部分是如何一起工作的。
- 用注释来总结代码块,使读者不致迷失在细节中。
写出言简意赅的注释
如果你要写注释,最好把它写得精确——越明确和细致越好。另外,由于注释在屏幕上也要占很多的地方,并且需要花更多的时间来读,因此,注释也需要很紧凑。
关键思想
- 注释应当有很高的信息/空间率。
- 避免使用不明确的代词
总结
本章是关于如何把更多的信息装入更小的空间里。下面是一些具体的提示:
- 当像“it”和“this”这样的代词可能指代多个事物时,避免使用它们。
- 尽量精确地描述函数的行为。
- 在注释中用精心挑选的输入/输出例子进行说明。
- 声明代码的高层次意图,而非明显的细节。
- 用嵌入的注释(如
Function(/*arg =*/...)
)来解释难以理解的函数参数。 - 用含义丰富的词来使注释简洁。
把控制流变得易读
关键思想
把条件、循环以及其他对控制流的改变做得越“自然”越好。运用一种方式使读者不用停下来重读你的代码。
条件语句中参数的顺序
- 比较的左侧 “被问询的”表达式,它的值更倾向于不断变化
- 比较的右侧 用来做比较的表达式,它的值更倾向于常量
?:条件表达式(又名“三目运算符”)
在类C的语言中,可以把一个条件表达式写成cond ? a : b这样的形式,其实就是一种对if (cond) { a } else {b }的紧凑写法。它对于可读性的影响是富有争议的。拥护者认为这种方式可以只写一行而不用写成多行。反对者则说这可能会造成阅读的混乱而且很难用调试器来调试。
关键思想
相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间。
- 默认情况下都用if/else。三目运算符?:只有在最简单的情况下使用。
- 避免do/while循环
- 我的经验是,do语句是错误和困惑的来源……我倾向于把条件放在“前面我能看到的地方”。其结果是,我倾向于避免使用do语句。
- 最小化嵌套
- 嵌套很深的代码很难以理解。每个嵌套层次都在读者的“思维栈”上又增加了一个条件。当读者见到一个右大括号(})时,可能很难“出栈”来回忆起它背后的条件是什么。
关键思想
- 当你对代码做改动时,从全新的角度审视它,把它作为一个整体来看待。
- 通过提早返回来减少嵌套
拆分超长的表达式
巨型乌贼是一种神奇而又聪明的动物,但它近乎完美的身体设计有一个致命的弱点:在它的食管附近围绕着圆环形的大脑。所以如果它一次吞太多的食物,它的大脑会受到伤害。
关键思想
把你的超长表达式拆分成更容易理解的小块。
- 用做解释的变量
- 拆分表达式最简单的方法就是引入一个额外的变量,让它来表示一个小一点的子表达式。这个额外的变量有时叫做“解释变量”,因为它可以帮助解释子表达式的含义。
- 总结变量
- 即使一个表达式不需要解释(因为你可以看出它的含义),把它装入一个新变量中仍然有用。我们把它叫做总结变量,它的目的只是用一个短很多的名字来代替一大块代码,这个名字会更容易管理和思考。
滥用短路逻辑
关键思想
要小心“智能”的小代码段——它们往往在以后会让别人读起来感到困惑。
总结
很难思考巨大的表达式。本章给出了几种拆分表达式的方法,以便读者可以一段一段地消化。
一个简单的技术是引入“解释变量”来代表较长的子表达式。这种方式有三个好处:
- 它把巨大的表达式拆成小段。
- 它通过用简单的名字描述子表达式来让代码文档化。
- 它帮助读者识别代码中的主要概念。
- 另一个技术是用德摩根定理来操作逻辑表达式——这个技术有时可以把布尔表达式用更整洁的方式重写(例如if (!(a && !b))变成if (!a || b))。
变量与可读性
在本章里,你会看到对于变量的草率运用如何让程序更难理解。确切地说,我们会讨论三个问题:
- 变量越多,就越难全部跟踪它们的动向。
- 变量的作用域越大,就需要跟踪它的动向越久。
- 变量改变得越频繁,就越难以跟踪它的当前值。
缩小变量的作用域
关键思想
让你的变量对尽量少的代码行可见。
只写一次的变量更好
关键思想
操作一个变量的地方越多,越难确定它的当前值。
总结
本章是关于程序中的变量是如何快速累积而变得难以跟踪的。你可以通过减少变量的数量和让它们尽量“轻量级”来让代码更有可读性。具体有:
- 减少变量,即那些妨碍的变量。我们给出了几个例子来演示如何通过立刻处理结果来消除“中间结果”变量。
- 减小每个变量的作用域,越小越好。把变量移到一个有最少代码可以看到它的地方。眼不见,心不烦。
- 只写一次的变量更好。那些只设置一次值的变量(或者const、final、常量)使得代码更容易理解。
重新组织代码
该部分会讨论可以在函数级别对代码做的更大的改动。具体来讲,我们会讲到三种组织代码的方法:
- 抽取出那些与程序主要目的“不相关的子问题”。
- 重新组织代码使它一次只做一件事情。
- 先用自然语言描述代码,然后用这个描述来帮助你找到更整洁的解决方案。
抽取不相关的子问题
本章的建议是“积极地发现并抽取出不相关的子逻辑”。我们是指:
- 看看某个函数或代码块,问问你自己:这段代码高层次的目标是什么?
- 对于每一行代码,问一下:它是直接为了目标而工作吗?这段代码高层次的目标是什么呢?
- 如果足够的行数在解决不相关的子问题,抽取代码到独立的函数中。
创建大量通用代码
通用代码很好,因为“它完全地从项目的其他部分中解耦出来”。像这样的代码容易开发,容易测试,并且容易理解。想象一下如果你所有的代码都如此会怎样!
简化已有接口
人人都爱提供整洁接口的库——那种参数少,不需要很多设置并且通常只需要花一点工夫就可以使用的库。它让你的代码看起来优雅:简单而又强大。但如果你所用的接口并不整洁,你还是可以创建自己整洁的“包装”函数。
总结
对本章一个简单的总结就是“把一般代码和项目专有的代码分开”。其结果是,大部分代码都是一般代码。通过建立一大组库和辅助函数来解决一般问题,剩下的只是让你的程序与众不同的核心部分。
这个技巧有帮助的原因是它使程序员关注小而定义良好的问题,这些问题已经同项目的其他部分脱离。其结果是,对于这些子问题的解决方案倾向于更加完整和正确。你也可以在以后重用它们。
一次只做一件事
关键思想
应该把代码组织得一次只做一件事情。
把想法变成代码
在本章中,我们会用一个简单的过程来使你编写更清晰的代码:
- 像对着一个同事一样用自然语言描述代码要做什么。
- 注意描述中所用的关键词和短语。
- 写出与描述所匹配的代码。
少写代码
关键思想
最好读的代码就是没有代码。
别费神实现那个功能——你不会需要它
程序员还倾向于低估实现一个功能所要花的工夫。我们乐观地估计了实现一个粗糙原型所要花的时间,但是忘记了在将来代码库的维护、文件以及后增的“重量”所带来的额外时间。
保持小代码库
最好的解决办法就是“让你的代码库越小,越轻量级越好”,就算你的项目在增长。那么你就要:
- 创建越多越好的“工具”代码来减少重复代码(见第10章)。
- 减少无用代码或没有用的功能(见下图)。
- 让你的项目保持分开的子项目状态。
- 总的来说,要小心代码的“重量”。让它保持又轻又灵。
熟悉你周边的库
很多时候,程序员就是不知道现有的库可以解决他们的问题。或者有时,它们忘了库可以做什么。知道你的库能做什么以便你可以使用它,这一点很重要。
这里有一条比较中肯的建议:每隔一段时间,花15分钟来阅读标准库中的所有函数/模块/类型的名字。这包括C++标准模板库(STL)、Java API、Python内置的模块以及其他内容。
总结
冒险、兴奋——绝地武士追求的并不是这些。——尤达大师
你可以通过以下方法避免编写新代码:
- 从项目中消除不必要的功能,不要过度设计。
- 重新考虑需求,解决版本最简单的问题,只要能完成工作就行。
- 经常性地通读标准库的整个API,保持对它们的熟悉程度。
使测试易于阅读和维护
关键思想
测试应当具有可读性,以便其他程序员可以舒服地改变或者增加测试。
当测试代码多得让人望而止步,会发生下面的事情:
- 程序员会不敢修改真实代码。“啊,我们不想纠结于那段代码,更新它的那些测试将会是个噩梦!”
- 当增加新代码时,程序员不会再增加新的测试。一段时间后,测试的模块越来越少,你不再对它有信心。
- 相反,你希望鼓励你代码的使用者(尤其是你自己!)习惯于测试代码。他们应该能在新改动破坏已有测试时做出分析,并且应该感觉增加新测试很容易。
选择好的测试输入
关键思想
基本原则是,你应当选择一组最简单的输入,它能完整地使用被测代码。
关键思想
又简单又能完成工作的测试值更好。
总结
以下是如何改进测试的几个具体要点:
- 每个测试的最高一层应该越简明越好。最好每个测试的输入/输出可以用一行代码来描述。
- 如果测试失败了,它所发出的错误消息应该能让你容易跟踪并修正这个bug。
- 使用最简单的并且能够完整运用代码的测试输入。
- 给测试函数取一个有完整描述性的名字,以使每个测试所测到的东西很明确。不要用Test1(),而用像
Test_<FunctionName>_<Situation>
这样的名字。 - 最重要的是,要使它易于改动和增加新的测试。
深入阅读
关于写高质量代码的书
- 《Code Complete: A Practical Handbook of Software Constructionm, 2nd edition》,by Steve McConnell(Microsoft Press, 2004)
- 一本严谨的大部头,是关于软件建构的所有方面的,包括代码质量以及其他。
- 《Refactoring: Improving the Design of Existing Code》, by Martin Fowler et al.(Addison-Wesley Professional, 1999)
- 一本关于增量代码改进哲学的好书,包含很多不同重构方法的具体分类,以及要在尽管不破坏东西的情况下做出这些改动所需的步骤。
- 《The Practice of Programming》, by Brian Kernighan and Rob Pike(Addison-Wesley Professional, 1999)
- 讨论了编程的多个方面,包含调试、测试、可移植性和性能,有很多代码示例。
- 《The Pragmatic Programmer: From Journeyman to Master》, by Andrew Hunt and David Thomas(Addison-Wesley Professional, 1999)
- 一系列好的编程和工程原则,按短小的讨论来组织。
- 《Clean Code: A Handbook of Agile Software Craftsmanship》, by Robert C.
Martin(Prentice Hall, 2008)- 和本书类似(但是专门为Java),还拓展了其他如错误处理和并发等话题。
关于各种编程话题的书
- 《JavaScript: The Good Parts》, by Douglas Crockford(O’Reilly, 2008)
- 我们认为这本书的精神与我们的书相似,尽管该书不是直接关于可读性的。它是关于如何使用JavaScript语言中不容易出错而且更容易理解的一个清晰子集的。
- 《Effective Java, 2nd edition》, by Joshua Bloch(Prentice Hall, 2008)
- 一本杰出的书,是关于让你的Java程序更易读和更少bug的。尽管它是关于Java的,但其中很多原则对所有的语言都适用。强烈推荐。
- 《Design Patterns: Elements of Reusable Object-Oriented Software》, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides(Addison-Wesley Professional, 1994)
- 这本书是软件工程师用来讨论面向对向编程所用的“模式”这种通用语言的原始出处。作为通用的、有用的模式的一览,它帮助程序员在第一次自己解决棘手问题时避免经常出现的陷阱。
- 《Programming Pearls, 2nd edition》, by Jon Bentley(Addison-Wesley Professional,1999)
- 关于真实软件问题的一系列文章。每一章都有解决真实世界中问题的真知灼见。
- 《High Performance Web Sites》, by Steve Souders(O’Reilly, 2007)
- 尽管这不是一本关于编程的书,但这本书也值得注意,因为它描述了几种不需要写很多代码就可优化网站的方法(与本书第13章的目的一致)。
- 《Joel on Software: And on Diverse and…》, by Joel Spolsky
- 来自于http://www.joelonsoftware.com/的一些优秀文章。Spolsky的作品涉及软件工程的很多方面,并且对很多相关话题都深有见解。
- 一定要读一读“Things You Should Never Do, Part I”和“The Joel Test: 12 Steps to BetterCode”。
我们发自肺腑地赞同Knuth的说法:“与其把我们主要的任务想象成指示计算机做什么,不如让我们关注解释给人类我们希望让计算机做什么”