... 是否要采用这些规范是由你和你的团队决定的。但不论你用哪个系统,在你的项目中要 保持一致。 总结 本章唯一的主题是:把信息塞入名字中。这句话的含意是,读者仅通过读到名字就可以 获得大量信息。 下面是讨论过的几个小提示: 使用专业的单词 • ——例如,不用Get,而用Fetch或者Download可能会更好,这由上 下文决定。 避免空泛的名字 • ,像tmp和retval,除非使用它们有特殊的理由。 26 第2章 使用具体的名字来更细致地描述事物 • ——S e r v e r C a n S t a r t()这个名字就比 CanListenOnPort更不清楚。 给变量名带上重要的细节 • ——例如,在值为毫秒的变量后面加上_ms,或者在还需要 转义的,未处理的变量前面加上raw_。 为作用域大的名字采用更长的名字 • ——不要用让人费解的一个或两个字母的名字来 命名在几屏之间都可见的变量。对于只存在于几行之间的变量用短一点的名字更 好。 有目的地使用大小写、下划线等 • ——例如,你可以在类成员和局部变量后面加上 "_"来区分它们。 27 第3章 不会误解的名字 不是右边的线, 是吧?!! 是的, 不是!! 28 第3章 在前一章中,我们讲到了如何把信息塞入名字中。本章会关注另一个话题:小心可能会 有歧义的名字。 关键思想 要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”要仔细审视这个 名字。 如果想更有创意一点,那么可以主动地寻找“误解点”。这一步可以帮助你发现那些二 义性名字并更改。 例如,在本章中,当我们讨论每一个可能会误解的名字时,我们将在心里默读,然后挑 选更好的名字。 例子: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)。 不会误解的名字 29 然而,参数名length也不太好。如果叫max_length的话可能会更清楚。 这样也还没有完。就算是max_length这个名字也还是会有多种解读: 字节数 • 字符数 • 字数 • 如你在前一章中所见,这属于应当把单位附加在名字后面的那种情况。在本例中,我们 是指“字符数”,所以不应该用max_length,而要用max_chars。 推荐用min和max来表示(包含)极限 假设你的购物车应用程序最多不能超过10件物品: CART_TOO_BIG_LIMIT = 10 if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT: Error("Too many items in cart.") 这段代码有个经典的“大小差一”缺陷。我们可以简单地通过把>=变成>来改正它: if shopping_cart.num_items() > CART_TOO_BIG_LIMIT: (或者通过把CART_TOO_BIG_LIMIT变成11)。但问题的根源在于 CART_TOO_BIG_LIMIT是 个二义性名字,它的含义到底是“少于”还是“少于/且包括”。 建议 命名极限最清楚的方式是在要限制的东西前加上max_或者min_。 在本例中,名字应当是MAX_ITEMS_IN_CART,新代码现在变得简单又清楚: MAX_ITEMS_IN_CART = 10 if shopping_cart.num_items() > MAX_ITEMS_IN_CART: Error("Too many items in cart.") 推荐用first和last来表示包含的范围 a b c d first last 30 第3章 下面是另一个例子,你没法判断它是“少于”还是“少于且包含”: 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来表示包含/排除范围 a b c d begin end 在实践中,很多时候用包含/排除范围更方便。例如,如果你想打印所有发生在10月16日 的事件,那么写成这样很简单: PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am") 这样写就没那么简单了: PrintEventsInRange("OCT 16 12:00am", "OCT 16 11:59:59.9999pm") 因此对于这些参数来讲,什么样的一对名字更好呢?对于命名包含/排除范围典型的编程 规范是使用begin/end。 但是end这个词有点二义性。例如,在句子“我读到这本书的end部分了”,这里的end是 包含的。遗憾的是,英语中没有一个合适的词来表示“刚好超过最后一个值”。 因为对begin/end的使用是如此常见(至少在C++标准库中是这样用的,还有大多数需要 “分片”的数组也是这样用的),它已经是最好的选择了。 给布尔值命名 当为布尔变量或者返回布尔值的函数选择名字时,要确保返回t r u e和f a l s e的意义很 明确。 不会误解的名字 31 下面是个危险的例子: bool read_password = true; 这会有两种截然不同的解释: 我们需要读取密码。 • 已经读取了密码。 • 在本例中,最好避免用“read”这个词,用need_password或者user_is_authenticated这 样的名字来代替。 通常来讲,加上像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()的实现是要遍历所有经过的数据并同时计算中值。如果有大量 32 第3章 的数据的话,这样的一步可能会有很大的代价!但一个容易轻信的程序员可能会随意地 调用getMean(),还以为这是个没什么代价的调用。 相反,这个方法应当重命名为像computeMean()这样的名字,后者听起来更像是有些代价 的操作。(另一种做法是,用新的实现方法使它真的成为一个轻量级的操作。) 例子:list::size() 下面是一个来自C++标准库中的例子。曾经有个很难发现的缺陷,使得我们的一台服务 器慢得像蜗牛在爬,就是下面的代码造成的: void ShrinkList(list& 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)。 向导是谁 一段时间以前,有位作者正在安装OpenBSD操作系统。在磁盘格式化这一步 时,出现了一个复杂的菜单,询问磁盘参数。其中的一个选项是进入“向导 模式”(Wizard mode)。他看到这个友好的选择松了一口气,并选择了它。 让他失望的是,安装程序给出了低层命名行提示符等待手动输入磁盘格式化 命令,而且也没有明显的方法可以退出。很明显,这里的“向导”指的是你 自己。 不会误解的名字 33 例子:如何权衡多个备选名字 当你要选一个好名字时,可能会同时考虑多个备选方案。通常你要在头脑中盘算一下每 个名字的好处,然后才能得出最后的选择。下面的例子示范了这个评判过程。 高流量网站常常用“试验”来测试一个对网站的改变是否会对业务有帮助。下面的例子 是一个配置文件,用来控制某些试验: experiment_id: 100 description: "increase font size to 14pt" traffic_fraction: 5% ... 每个试验由15对属性/值来定义。遗憾的是,当要定义另一个差不多的试验时,你不得不 拷贝和粘贴其中的大部分。 experiment_id: 101 description: "increase font size to 13pt" [other lines identical to experiment_id 100] 假设我们希望改善这种情况,方法是让一个试验重用另一个的属性(这就是“原型继 承”模式)。其结果是你可能会写出这样的东西: experiment_id: 101 the_other_experiment_id_I_want_to_reuse: 100 [change any properties as needed] 问题是:the_other_experiment_id_I_want_to_reuse到底应该如何命名?下面有4个名 字供考虑: 1. template 2. reuse 3. copy 4. inherit 所有的这些名字对我们来讲都有意义,因为是我们把这个新功能加入配置语言中的。但 我们要想象一下对于看到这段代码却又不知道这个功能的人来讲,这个名字听起来是什 么意思。因此我们要分析每一个名字,考虑各种让人误解的可能性。 1. 让我们想象一下使用这个名字模板时的情形: experiment_id: 101 template: 100 ... template有两个问题。首先,我们不是很清楚它的意思是“我是一个模板”还是“我 34 第3章 在用其他模板”。其次,“template”常常指代抽象事物,必须要先“填充”之后才 会变“具体”。有人会以为一个模板化了的试验不再是一个“真正的”试验。总之, template对于这种情况来讲太不明确。 2. 那么reuse呢? experiment_id: 101 reuse: 100 ... reuse这个单词还可以,但有人会以为它的意思是“这个试验最多可以重用100次”。把 名字改成reuse_id会好一点。但有的读者可能会以为reuse_id的意思是“我重用的id是 100”。 3. 让我们再考虑一下copy。 experiment_id: 101 copy: 100 ... copy这个词不错。但copy:100看上去像是在说“拷贝这个试验100次”或者“这是什么东 西的第100个拷贝”。为了确保明确地表达这个名字是引用另一个试验,我们可以把名 字改成copy_experiement。这可能是到目前为止最好的名字了。 4. 但现在我们再来考虑一下inherit: experiment_id: 101 inherit: 100 ... 大多数程序员都熟悉“inherit”(继承)这个词,并且都理解在继承之后会有进一步的 修改。在类继承中,你会从另一个类中得到所有的方法和成员,然后修改它们或者添加 更多内容。甚至在现实生活中,我们说从亲人那里继承财产,大家都理解你可能会卖掉 它们或者再拥有更多属于你自己的东西。 但是如果要明确它是继承自另一个试验,我们可以把名字改进成inherit_from,或者甚 至是inherit_from_experiement_id。 综上所述,copy_experiment和inherit_from_experiment_id是最好的名字,因为它们对 所发生的事情描述最清楚,并且最不可能误解。 总结 不会误解的名字是最好的名字——阅读你代码的人应该理解你的本意,并且不会有其 他的理解。遗憾的是,很多英语单词在用来编程时是多义性的,例如filter、length和 limit。 不会误解的名字 35 在你决定使用一个名字以前,要吹毛求疵一点,来想象一下你的名字会被误解成什么。 最好的名字是不会误解的。 当要定义一个值的上限或下限时,max_和min_是很好的前缀。对于包含的范围,first和 last是好的选择。对于包含/排除范围,begin和end是最好的选择,因为它们最常用。 当为布尔值命名时,使用is和has这样的词来明确表示它是个布尔值,避免使用反义的词 (例如disable_ssl)。 要小心用户对特定词的期望。例如,用户会期望get()或者size()是轻量的方法。 “软件开发的一个重要部分是要意识到你的代码以后将如何影响查看这些代码的人。两位作者 高屋建瓴,带你领略这一挑战的各个方面,并且使用有指导意义的例子来解释细节。” ——Michael Hunger,软件开发人员 细节决定成败,思路清晰、言简意赅的代码让程序员一目了然;而格式凌乱、拖沓冗长的代码 让程序员一头雾水。除了可以正确运行以外,优秀的代码必须具备良好的可读性,编写的代码 要使其他人能在最短的时间内理解才行。本书旨在强调代码对人的友好性和可读性。 本书关注编码的细节,总结了很多提高代码可读性的小技巧,看似都微不足道,但是对于整个 软件系统的开发而言,它们与宏观的架构决策、设计思想、指导原则同样重要。编码不仅仅只 是一种技术,也是一门艺术,编写可读性高的代码尤其如此。如果你要成为一位优秀的程序员, 要想开发出高质量的软件系统,必须从细处着手,做到内外兼修,本书将为你提供有效的指导。 主要内容: ■ 简化命名、注释和格式的方法,使每行代码都言简意赅。 ■ 梳理程序中的循环、逻辑和变量来减小复杂度并理清思路。 ■ 在函数级别解决问题,例如重新组织代码块,使其一次只做一件事。 ■ 编写有效的测试代码,使其全面而简洁,同时可读性更高。 The Art of Readable Code 软件开发/编程 Boswell & Foucher Dustin Boswell毕业于加州理工大学,资深软件工程师,在Google就职多年,负责Web爬虫和程 序设计相关的工作。他专注于前端、后端,服务器架构、机器学习、大数据、系统和网站等技 术领域的研究和实践,经验十分丰富。他现在是MyLikes的软件工程师。 Trevor Foucher资深软件工程师和技术经理,先后在Microsoft和Google工作了数十年,在 Microsoft担任软件工程师、技术经理以及安全产品技术主管,在Google从事广告应用开发和搜 索基础结构研发相关的工作。 oreilly.com.cn 编写可读代码的艺术 编 写 可 读 代 码 的 艺 术 编写可读代码的艺术 著 此简体中文版仅限于在中华人民共和国境内(但不允许在中国香港、澳门特别行政区和中国台湾地区)销售发行 This Authorized Edition for sale only in the territory of People's Republic of China (excluding Hong Kong, Macao and Taiwan) O'Reilly Media, Inc.授权机械工业出版社出版 定价:59.00元 客服热线:(010) 88378991,88361066 购书热线:(010) 68326294,88379649,68995259 投稿热线:(010) 88379604 读者信箱:hzjsj@hzbook.com 华章网站:http://www.hzbook.com 网上购书:www.china-pub.com 9 ISBN 978-7-111-38544-8 787111 385448 机 械 工 业 出 版 社 Dustin Boswell & Trevor Foucher 著 尹哲 郑秀雯 译