Back to Top

读书笔记《编写可读代码的艺术》

在去上海参加Unite2016的来回飞机上,读了这本《编写可读代码的艺术》,学到了不少经验。 趁周末将笔记从kindle上导出

吐槽一下:kindle的 My Clippings.txt 不能保存在云端么?

代码应当易于理解

代码的写法应当使别人理解它所需的时间最小化。

把信息装到名字里

  • 用更精确的名字可能会有帮助。如果不把循环索引命名为(i、j、k),另一个选择可以是(club_i、members_i、user_i)或者,更简化一点(ci、mi、ui)。这种方式会帮助把代码中的缺陷变得更明显
  • 如果你的变量是一个度量的话(如时间长度或者字节数),那么最好把名字带上它的单位。例如,在值为毫秒的变量后面加上_ms。
  • 为作用域打的名字采用更长的名字–不要用让人费解的一个或者两个字母的名字来命名在几屏之间都可见的变量。对于只存在于几行之间的变量用短一点的名字更好。

不会误解的名字

要多问自己几遍:“这个名字会被别人解读成其他的含义?” 要仔细审视这个名字。

  • 推荐用Min和Max来表示(包含)极限。==> []
  • 推荐用First和Last来表示包含的范围。==> []
  • 推荐用Begin和End来表示包含/排除范围。==> [)
  • 通常来讲,加上像is、has、can或should这样的词,可以把布尔值变得更明确。
  • 不会误解的名字是最好的名字——阅读你代码的人应该理解你的本意,并且不会有其他的理解。

审美

一致的风格比“正确”的风格更重要。

通过把代码用一致的、有意义的方式“格式化”,可以把代码变得更容易读,并且可以读得更快。

  • 如果多个代码块做相似的事情,尝试让它们有同样的剪影。 使用相同的格式
  • 把代码按“列”对齐可以让代码更容易浏览。
  • 如果在一段代码中提到A、B和C,那么不要在另一段中说B、C和A。选择一个有意义的顺序,并始终用这样的顺序。
  • 用空行来把大块代码分成逻辑上的“段落”。

该写什么样的注释

什么地方不需要注释:

  • 能从代码本身中迅速地推断的事实。
  • 用来粉饰烂代码(例如蹩脚的函数名)的“拐杖式注释”——应该把代码改好。
  • 你应该记录下来的想法包括:
  • 对于为什么代码写成这样而不是那样的内在理由(“指导性批注”)。
  • 代码中的缺陷,使用像TODO:或者XXX:这样的标记。
  • 常量背后的故事,为什么是这个值。

站在读者的立场上思考:

  • 预料到代码中哪些部分会让读者说:“啊?”并且给它们加上注释。
  • 为普通读者意料之外的行为加上注释。
  • 在文件/类的级别上使用“全局观”注释来解释所有的部分是如何一起工作的。
  • 用注释来总结代码块,使读者不致迷失在细节中。

把控制流变得易读

  • 在写一个比较时(while(bytes_expected>bytes_received)),把改变的值写在左边并且把更稳定的值写在右边更好一些(while(bytes_received<bytes_expected))

    比较的左侧 比较的右侧
    “被问询的”表达式,它的值更倾向于不断变化 用来作比较的表达式,它的值更倾向于常量

    这条指导原则和英语的用法一致。 我们会很自然地说:“如果你的年收入至少是10万美元”或者“如果你不小于18岁。” 而“如果18岁小于或等于你的年龄”这样的说法却很少见。

  • 嵌套的代码块需要更加集中精力去理解。每层新的嵌套都需要读者把更多的上下文“压入栈”。应该把它们改写成更加“线性”的代码来避免深嵌套。
  • 通常来讲提早返回可以减少嵌套并让代码整洁。“保护语句”(在函数顶部处理简单的情况时)尤其有用。

本章介绍低层次控制流:如何把循环、条件和其他跳转写得简单易读。但是你也应该从高层次来考虑程序的“流动”。理想的情况是,整个程序的执行路径都很容易理解——从main()开始,然后在脑海中一步步执行代码,一个函数调用另一个函数,直到程序结束。

然而在实践中,编程语言和库的结构让代码在“幕后”运行,或者让流程难以理解。下面是一些例子:

编程结构 高层次程序流程是如何变得不清晰的
线程 不清楚什么时间执行什么代码
信号量、中断处理程序 有些代码随时都有可能执行
异常 可能会从多个函数调用中向上冒泡一样执行
函数指针和匿名函数 很难知道到底会执行什么代码,因为在编译时还没有决定
虚方法 object.virtualMethod() 可能会调用一个未知子类的代码

拆分超长的表达式

一个简单的技术是引入“解释变量”来代表较长的子表达式。这种方式有三个好处:

  • 它把巨大的表达式拆成小段。
  • 它通过用简单的名字描述子表达式来让代码文档化。
  • 它帮助读者识别代码中的主要概念。

另一个技术是用德摩根定理来操作逻辑表达式——这个技术有时可以把布尔表达式用更整洁的方式重写。例如 if(!(a && !b)) 变成 if(!a||b)

变量与可读性

通过减少变量的数量和让它们尽量“轻量级”来让代码更有可读性。具体有:

  • 减少变量,即那些妨碍的变量。我们给出了几个例子来演示如何通过立刻处理结果来消除“中间结果”变量。
  • 减小每个变量的作用域,越小越好。把变量移到一个有最少代码可以看到它的地方。眼不见,心不烦。
  • 只写一次的变量更好。那些只设置一次值的变量(或者const、final、常量)使得代码更容易理解。

抽取不相关的子问题

“把一般代码和项目专有的代码分开”。 大部分代码都是一般代码。通过建立一大组库和辅助函数来解决一般问题,剩下的只是让你的程序与众不同的核心部分。

它使程序员关注小而定义良好的问题,这些问题已经同项目的其他部分脱离。 对于这些子问题的解决方案倾向于更加完整和正确。你也可以在以后重用它们。

把想法变成代码

  • 用自然语言描述程序然后用这个描述来帮助你写出更自然的代码。这个技巧出人意料地简单,但很强大。看到你在描述中所用的词和短语还可以帮助你发现哪些子问题可以拆分出来。
  • 橡皮鸭技术
  • 如果你不能把问题说明白或者用词语来做设计,估计是缺少了什么东西或者什么东西缺少定义。把一个问题(或想法)变成语言真的可以让它更具体。

少写代码

写越少代码越好。每行新的代码都需要测试、写文档和维护。另外,代码库中的代码越多,它就越“重”,而且在其上开发就越难。

  • 从项目中消除不必要的功能,不要过度设计。
  • 重新考虑需求,解决版本最简单的问题,只要能完成工作就行。
  • 经常性地通读标准库的整个API,保持对它们的熟悉程度。 每隔一段时间,花15分钟来阅读标准库中的所有函数/模块/类型的名字。这包括C++标准模板库(STL)、Java API、Python内置的模块以及其他内容。

测试与可读性

可测试行差的代码特征,以及它所带来的设计问题

特征 可测试性的问题 设计问题
使用全局变量 对于每个测试都要重置所有的全局状态(否则不同的测试之间会互相影响) 很难理解哪些函数有什么副作用。没办法独立考虑每个函数,要考虑整个程序才能理解是不是所有的代码都能工作
对外部组件有大量依赖的代码 很难给它写出任何测试,因为要先搭起太多的脚手架。写测试会比较无趣,因此人们会避免写测试 系统更可能因某一依赖失败而失败。对于改动来讲很难知道会产生什么样的影响。很难重构类,系统会有更多的失败模式并且要考虑更多恢复路径
代码有不确定的行为 测试会很古怪,而且不可靠。经常失败的测试最终会被忽略 这种程序更可能会有条件竞争或者其他难以重现的bug。这种程序很难推理。产品中的bug很难跟踪和改正。

可测试性较好的代码特征,以及它所产生的优秀设计

特征 对可测试性的好处 对设计的好处
类中只有很少或者没有没有内部状态 很容易写出测试,因为要测试一个方法只要较少的设置,并且有较少的隐藏状态需要检查 有较少状态的类更简单,更容易理解。
类/函数只做一件事 要测试它只需要较少的测试用例 较小/较简单的组件更加模块化,并且一般来讲系统有更少的耦合
每个类对别的类依赖很少;低耦合 每个类可以独立地测试(比多个类一起测试容易得多) 系统可以并行开发。可以很容易修改或者删除类,而不会影响系统的其他部分
函数的接口简单,定义明确 有明确的行为可以测试。测试简单接口所需的工作量较少 接口更容易让程序员学习,并且重用的可能性更大

过度关注测试带来的问题

  • 牺牲真实代码的可读性,只是为了使能测试。
  • 着迷于100%的测试覆盖率。测试你代码的前面90%通常要比那后面的10%所花的工夫少。后面那10%包括用户接口或者很难出现的错误情况,其中bug的代价并不高,花工夫来测试它们并不值得。
  • 过度关注使测试成为产品开发的阻碍,测试本应只是项目的一个方面,却主导了整个项目。