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

这是我刚入职的时候,mentor 推荐的一本书。当时的代码写得真的挺糟糕的,根本不知道什么是好的代码。这本书帮我建立了一个模糊的标准,知道写好代码应该有哪几个可以努力的方向。最近又重读了一遍,很多之前不能完全理解的内容慢慢开始理解,以前觉得自己懂了的内容又有了新的认识。

img

软件成本由开发成本与维护成本组成,而往往维护成本要远高于开发成本。这其中耗费的主要成本就是由于理解代码和修改代码造成的。

​ ——— 《Structured Design》

代码应当易于理解

可读性基本定理

  • 代码的写法应当使别人理解它所需的时间最小化
  • 那个“其他人”可能就是 6 个月后的你自己,那时你自己的代码看上去已经很陌生了

代码总是越小越好吗?

  • 相比减少代码行数,把理解代码所需的时间最小化是一个更好的目标

理解代码所需的时间是否与其他目标有冲突

  • 让你的代码容易理解往往会把它引向好的架构且容易测试

表面层次的改进

选择好的名字、写好的注释以及把代码整洁地写成更好的格式。

1. 把信息装到名字里

1. 选择专业的词

  • “把信息装入名字中”包括要选择非常专业的词,避免使用“空洞”的词

  • 找到更有表现力的词:用于使用同义词典或者问朋友更好的名字建议

2. 避免泛泛的名字

  • 避免使用像 tmp 或 retval 这样的名字
  • 好的名字应当描述变量的目的或者它所承载的值
  • tmp 这个名字只应用于短期存在且临时性为其主要存在因素的变量
  • 循环迭代器中长使用 i、 j、 iter、 it 等名字做索引,但在嵌套的循环迭代器中,可以将索引的第一个字母与数据的第一个字符匹配

3. 用具体的名字替代抽象的名字

  • 把名字描述得更具体而不是更抽象
  • 因为一个名字含糊婉转而选择它可能不是一个好主意

4. 使用前缀或后缀来给名字附带更多信息

  • 一个变量名就像是一个小小的注释
  • 如果变量是一个度量的话,最后把名字带上它的单位
  • 对于变量存在危险或意外的时候都应该附带上额外信息
  • 匈牙利表示法:把变量的“类型”信息编写进名字的前缀

5. 决定名字的长度

  • 选择好名字时,有一个隐含的约束是名字不能太长
  • 在小的作用域里可以使用短的名字
    • 所有的信息(变量的类型、初值等)都很容易看到,所以可以用很短的名字
  • “不会输入”不再是避免使用长名字的理由,编辑器的补全功能可以解决这个问题
  • 使用项目所特有的缩写词是糟糕的选择——经验原则是:团队的新成员是否能理解这个名字的含义
  • 丢掉没用的词——拿掉后不会损失任何信息的单词

6. 利用名字的格式来表达含义

  • 对于下划线、连字符和大小写的使用方式可以把更多信息装到名字中
  • 对不同的实体使用不同的格式,能帮你更容易地阅读代码

2. 不会误解的名字

  • 要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”也仔细审视这个名字——可以主动寻找“误解点”
  • 推荐用 min 和 max 来表示(包含)极限
  • 推荐用 first 和 last 来表示包含的范围
    • 尽管 start 是个合理的参数名,但 stop 可以有多重解读。对于这样包含的范围(这种范围包含开头和结尾),一个好的选择是 first/last
  • 推荐用 begin 和 end 来表示包含/排除范围
  • 当为布尔变量或者返回布尔值的函数选择名字时,要确保返回 true 和 false 的意义很明确
    • 加上像 is、 has、 can 或 should 这样的词,可以把布尔值变得更明确
    • 最后避免使用反义名字
  • 避免使用用户对它的含义有先入为主印象的名字,从而导致误解

3. 审美

使用从审美角度讲让人愉悦的代码

三大原则

  1. 使用一致的布局,让读者很快习惯这种风格

  2. 让相似的代码看上去相似

  3. 让相关的代码行分组,形成代码块

技巧

  • 重新安排换行来保持一致和紧凑
  • 用方法来整理不规则的东西——“看上去漂亮”通常会带来不限于表面层次的改进,它可能会帮你把代码的结构做得更好
  • 在需要时使用列对齐
  • 选择一个有意义的顺序,始终一致地使用它
    • 让变量的顺序与对应的 HTML 表单中的字段顺序相匹配
    • 从“最重要”到“最不重要”排序
    • 按字母顺序排序
  • 把声明按块组织起来
  • 把代码分成“段落”
  • 选择一种风格——选择哪种风格不会影响到代码的可读性,但是把两种风格混在一起就会影响了

4. 该写什么样的注释

注释的目的是尽量帮助读者了解得和作者一样多

什么不需要注释

  • 不要为那些从代码本身就能快速推断的事实写注释
  • 不要为了注释而注释——避免“没有价值的注释”
  • 不要给不好的名字加注释——应该把名字改好
    • 注释不应用于粉饰不好的名字
    • 一个好的名字比一个好的注释更重要,因为在任何用到这个函数的地方偶读能看得到它
    • 不要“拐杖式注释”——试图粉饰可读性差的代码的注释
    • 好代码 > 坏代码 + 好注释

用代码记录你的思想

  • 加入注释记录你对代码的有价值的见解
  • 为代码中的瑕疵写注释
    • TODO: 还没有处理的事情
    • FIXME: 已知的无法运行的代码
    • HACK: 对一个问题不得不采用的比较粗糙的解决方案
    • XXX: 危险!这里有重要的问题
  • 给常量加注释——它是什么或者为什么它是这个值

站在读者的角度,去详细他们需要知道什么

  • 给意料之中的提问加上注释
  • 可能的陷阱——预料人们使用你的代码时可能会遇到的问题
  • “全局观”注释——各部分如何一起工作
  • 总结性注释——避免读者迷失在细节中

5. 写出言简意赅的注释

注释应当有很高的信息/空间率

  • 让注释保持紧凑
  • 避免使用不明确的代词
  • 润色粗糙的句子
  • 精确地描述函数的行为
  • 用输入/输出例子来说明特别的情况
  • 声明代码的高层次意图,而非明显的细节
  • 用嵌入的注释来解释难以理解的函数参数
  • 用含义丰富的词来使注释简洁

简化循环和逻辑

1. 把控制流变得易读

  • 把条件、循环以及其他对控制流的改变做得越“自然”越好。运用一种方式使读者不用停下来重读你的代码。

条件语句中参数的顺序

  • 左侧更倾向于不断变化,右侧更倾向于常量

if/else 语句块的顺序

  • 首先处理正逻辑而不是负逻辑的情况
  • 先处理简单的情况
  • 先处理有趣的或者说可疑的情况

三目运算符

  • 相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间
  • 默认情况下都用 if/else。三目运算符只有在最简单的情况下使用

避免 do/while 循环

  • 一般会从前向后读代码,do/while由其后的一个条件决定是否执行,不太滋润
  • do/while 循环中的 continue 语句让人迷惑

从函数中提前返回

最小化嵌套

  • 当你对代码做改动时,从全新的角度审视它。把它当做一个整体来看待
  • 通过提前返回来减少嵌套
  • 减少循环内的嵌套

2. 拆分超长的表达式

  • 代码中的表达式越长,它就越难以理解
  • 把你的超长表达式拆分成更容易理解的小块

解释变量

  • 拆分表达式最简单的方法是引入一个额外的变量,让它来表示一个小一点的子表达式

总结变量

  • 用一个短很多的名字来替代一大块代码,会更容易管理和思考

德摩根定理

  • 分别取反,转换与/或
1
2
not (a or b or c) <=> (not a) and (not b) and (not c)
not (a and b and c) <=> (not a) or (not b) or (not c)

不滥用短路逻辑

  • 布尔操作会做短路计算
  • 要小心“智能”的小代码段——它们往往在以后会让别人读起来感到困惑

找到更优雅的方式

  • 看看能否从“反方向”解决问题

3. 变量与可读性

  • 变量越多,就越难全部跟踪它们的动向
  • 变量的作用域越大,就需要跟踪它的动向越久
  • 变量改变得越频繁,就越难以跟踪它的当前值

减少变量

  • 没有价值的临时变量
  • 减少中间结果
  • 减少控制流变量——没有保护任何程序的数据,只是控制程序的执行

缩小变量的作用域

  • 让你的变量对尽量少的代码行可见

只写一次的变量更好

  • 操作一个变量的地方越多,越难确定它的当前值
  • 中间变量通常可以通过提前返回来消除

重新组织代码

1. 抽取不相关的子问题

  • 积极地发现并抽取出不相关的子逻辑
  • 把一般代码和项目专有代码分开

通用代码

  • 从项目的其他部分解耦出来

2. 一次只做一件事

  • 应该把代码组织得一次只做一件事情

把想法变成代码

  • 用自然语言描述程序然后用这个描述来帮助你写出更自然的代码。

少写代码

  • 最好读的代码就是没有代码

质疑和拆分需求

  • 把需求削减成一个简单的问题,只需要较少的代码
  • 让你的代码块越小,越轻量级越好
    • 创建好的“工具”代码来减少重复代码
    • 减少无用代码或没有用的功能
    • 让你的项目保持分开的子项目状态
    • 小心代码的“重量”

熟悉周边的库

  • 每隔一段时间,花 15 分钟来阅读标准库中的所有函数/模块/类型的名字