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

122 阅读8分钟

Write Programs for People First,Computers Second 首先为人写程序,其次才是为机器 —— 《Code Complete 2》第 34.3 章

1、可读性基本原理

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

2、代码的可读性 和 代码性能目标是否有冲突?

高性能和可读性并不冲突,可读性好会让代码更容易被开发人员迭代,也容易被动态编译器优化!

  • “当你犹豫不决时,可读性基本定理总是优先于书中其他条例与原则”(「设计模式之美」专栏讲解可读性时也引用了这句话)

  • 《Java Concurrency in Practice》 作者 Brian Goetz 等人也是推荐优先可读性代码,除非能证明运行起来后性能达不到明确绩效指标要求,才考虑优化。

  • 《CSAPP》第五章(优化程序性能)中作者同样提到了可读性的重要并不亚于性能提升。

3、代码表面层次的改进

3.1、好的名字(承载着信息,能够见名知意)

准确的词、具体的名字: 能描述出目的,承载着信息

使用前后缀附带更多信息: 比如将 getTime 改为 getTimeMs

适当长度: 作用域越小的名字可以越短(因为能结合上下文快速理解)

好的格式风格: 遵守大家熟悉的格式规范(项目、团队应该使用一致的格式风格,最好能有指导文档进行规范);项目格式一致性规范比“正确”的风格更重要。

3.2、避免创建容易产生误解的名字

阅读你代码的人应该理解你的本意,并且不会有其他的理解。

命名推荐:

  • 定义上下限: max、min

  • 表示包含范围: first、last

  • 表示包含/排除范围: begin、end

  • 使用 is、has 来明确表示布尔值(注:Java Bean 中不能用 is 前缀命名布尔值,会导致序列化失败,所以还是建议少用 isXXX;如果要使用 isXXX 还得考虑下英语语法),避免使用反义词

3.3、好的注释

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

三个不要写:

  • 不要为能从代码本身快速推断的事实写注释: 注释会占用阅读真实代码的时间
  • 不要用注释来弥补在命名上的懒惰(拐杖式注释): 好的命名优于好的注释;
  • 不要写与真实代码不一致的错误注释: 真实代码变更后也要注意及时变更注释

六个建议写:

  • 适当记录自己的设计思想: 解释为什么要这样做,让读者能够快速理解自己的设计思路、设计原因
  • 为需要演进优化的代码备注: 让读者以及自己知道这段代码值得演进优化
  • 为常量加上注释: 常量的作用域往往非常大,命名并不一定能体现出它真实作用范围
  • 多站在读者的角度思考: 思考读者会对哪些代码产生疑惑,为这些代码添加注释。(但依旧要注意,不要写拐杖式注释,不要用注释来弥补代码的可读性缺陷)
  • 为容易产生误解/有陷阱的代码写上注释: 比如某个方法在某些场景性能很差并且难以优化,可以在注释上写上,避免使用者踩坑。
  • 为文件/函数写总结性代码: 总结性描述内部做的事情,能让读者站在全局角度快速熟悉整个项目

权衡写还是不写的核心思考:

  • 任何能帮忙读者理解代码的注释都可以考虑写: 优先写“为什么”,代码层面难以让读者快速理解“做什么”、“怎么做”的时候,也可以写上相关注释。

注释尽量言简意赅(无用注释 = 额外阅读成本)

  • 用 Example 注释代替函数行为注释

  • 精确概括出函数的意图(意图 = 函数目的),不要过多描述行为细节(细节可用好的 Example 注释 & 单元测试覆盖来代替)

4、简化程序的 循环和逻辑

循环与逻辑处理的代码越“自然”越好,让读者阅读代码时尽量不需要停下来重读代码。(也就是能尽量顺畅不阻塞,顺畅不阻塞 = 易理解)

控制流可读性写法推荐:

  • 做比较操作时,应该将不断变化的值写在左边,较为稳定的值写在右边。
  • “数组长度大/小于10吗?” 优于 “10 比数组长度大/小吗?”
  • 避免使用 do/while;除非语句特别简单,尽量不要使用 三目运算符,更不要嵌套使用。
  • 避免条件逻辑嵌套耦合(可采用拆分成函数、思考整体函数目的重新组织代码结构、配合提早 retrun 等方式优化)

拆分长表达式的可读性写法推荐:

  • 引入“解释变量”来表达子表达式,再将各个解释变量进行组合来表达更大的父表表达式。
  • 如果子表达式依旧很长,可以采用继续拆分的方式
  • 如果逻辑较为复杂,可将表达式拆分出函数

变量的可读性写法推荐(如何管理变量):

  • 减少变量(对提升可读性没有帮助的、改造前后发现没有什么区别、甚至删减后可读性更好的变量):没有价值的临时变量。

  • 缩小变量作用域(注意,这里指的是变量而不是常量):作用域确实比较大时可采用标记为 static、拆分类的方式进行优化。

  • 使用常量:只会有一个值的变量改为常量更好

5、重新组织函数代码

工程学:把大问题拆分为小问题再把这些问题的解决方案放回一起。

推荐:

  • 抽取出与程序主要目的“不相关的子问题”

  • 将专有代码和通用代码分离开(独有的业务逻辑代码和技术工具代码分离开)

  • 重新组织代码使它一次只做一件事情

  • 将一段很难读懂的代码逻辑重新梳理,然后尝试重新组织它,看是否能将其拆分为多个子问题的组合,为每个子问题定义一个函数(单一职责,函数/类/项目角度都能适用)

  • 先用自然语言描述代码,然后用描述来帮助找到更整洁的解决方案。(橡皮鸭技术)

  • 如果不能把问题说明白或者用词语来做设计,往往是缺少了东西或者定义。

  • 少写代码。(最好读的代码就是没有代码)

  • 删除没用的代码

  • 熟悉周边的库(项目内部,二方/三方库)

  • 每隔一段时间去阅读下经典库、常用库

6、测试可读性

测试代码应该也具有可读性:以便其他程序员可以舒服地改变或者增加测试。

推荐:

  • 对使用者隐去不重要的细节,以便更重要的细节会更突出。

  • 可利用辅助函数,将一些不是主逻辑的细节操作让辅助函数处理掉。(Java 的单元测试可以利用 before、after、注解、依赖注入方式等方式将无关主逻辑的细节处理掉)

  • 选择好的测试输入(应当选择最简单的输入,它能完整地使用被测代码;又简单又能完成工作的测试值最好)

  • 写多个小的测试多角度覆盖测试代码

  • 为测试函数命名,指明测试函数要测试的内容

  • 不要担心名字太长,测试方法在项目中不会被调用。

  • 个人建议:testXxx_xxx(测试某某方法_测试目的)

  • 名字应当能描述被测试的函数以及测试内容

易测试代码:

  • 易测试的代码:明确定义的接口,没有过多状态或者其他的设置,没有很多需要审查的隐藏数据等等。

  • 好处:很自然会产生有良好组织的代码,其中不同的部分做不同的事情。

  • 不易测试的代码:

  • 使用全局变量

  • 测试性问题:对于每个测试都要重置所有的全局状态

  • 设计问题:要考虑整个程序才能理解是不是所有的代码都能工作

  • 对外部组件大量依赖(高耦合)

  • 代码有不确定行为

避免过度设计:

  • 不要为了易测试,牺牲真实代码的可读性
  • 不要过于着迷 100% 覆盖率(边际效应,后面10%的收益过低,成本过高;除非代码确实非常核心,容不得一点错误,比如医院、飞船)