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

《编写可读代码的艺术》

前言

本书旨在帮助你把代码写得更好

程序员的日常工作的大部分时间都花在一些"基本"的事情上, 像是给变量命名、写循环以及在函数级别解决问题. 并且这其中很大一部分是阅读和编辑已有的代码.【毕竟很少情况下是独立开发一个系统, 二次开发的情况应该比较多】

第1章 代码应当易于理解

是什么让代码变得"更好"

可读性基本定理

代码的写法应当使别人理解它所需的时间最小化【那个"别人"可能就是6个月后的自己...其实可能不到1周后的...】

总是越小越好吗

代码短并不是易理解的标准, 2者基本没有关系. 如下面2段功能一致的代码:

assert(!(bucket = FindBucket(key)) || !bucket -> IsOccupied())

bucket = FindBucket(key);
if (bucket != null)
    assert(!bucket -> IsOccupied())

后面的明显比较容易理解.

尽管减少代码行数是一个好目标, 但把理解代码所需的时间最小化是一个更好的目标【添加注释有助于理解代码】

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

可读性基本定理总是先于本书中任何其他条例或原则.

最难的部分

要经常想其他人是否会觉得你的代码容易理解, 需要额外的时间

第一部分 表面层次的改进

(命名、注释以及审美[代码整洁] —— 可以用于代码库每一行的小提示)

第2章 把信息装到名字里(读者仅通过读到名字就可以获得大量信息)

把名字当做一条小小的注释, 可以承载很多信息

选择专业的词

例如, "get"这个词就非常不专业【不能了解是从哪"get"】, 更专业的名字可以是"fetch"或"download", 根据上下文决定

"size"很难让人理解到底是什么的"size"【确实, 经常不理解】

术语上区分它的职能、找到更有表现力的词

单词 更多选择
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("我是一个返回值")这样空泛的名字

(或者说要知道什么时候使用它)

好的名字应当描述变量的目的或者它所承载的值

建议:

  • retval这个名字没有包含很多信息. 用一个描述该变量的值的名字来代替它.
  • tmp这个名字只应用于短期存在且临时性为其主要存在因素的变量.(比如交换变量)
  • 循环迭代器: 索引的第一个字母应该与数据的第一个字符匹配【这是个好建议, 不单纯使用i,j,k,it,iter】
  • 对于空泛名字的裁定: 如果你需要使用像tmp、it或者retval这样空泛的名字, 那么你要有个好的理由

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

在给变量、函数或者其他元素命名时, 要把它描述的更具体而不是更抽象

例子: –run_locally(本地运行)

本意使程序输出更多的调试信息, 但会运行得更慢. 一般用于在本机上测试, 但当运行在服务器上, 性能很重要的时候, 一般不会使用这个标记

带来的问题:

  1. 新成员不明白真正含义, 可能认为是在本地运行的标记
  2. 偶尔我们需要在远程服务器上查看调试信息, 使用这个看上去比较滑稽
  3. 有时我们在本地运行性能测试, 不需要日志信息, 所以不能使用–run_locally

这种情况下,extra_logging这个意义会更好一点

为名字附带更多信息

(使用前缀或后缀)

16进制的id String id -> String hex_id

给变量名带上重要的细节 —— 例如, 在值为毫秒的变量后面加上_ms, 或者在还需要转义的, 未处理的变量前面加上raw_

如果关于一个变量有什么重要事情要读者必需知道, 那么是值得把额外的"词"添加到名字中的

带单位的值【带上单位就不用去查说明到底用的是什么单位了, 尤其是时间, 经常搞不清是使用秒还是毫秒】

delay_secssize_mbmax_kbpsdegrees_cw

附加其他重要属性

password | plaintext_password "纯文本"格式密码, 需要加密
comment  | unescaped_comment
html     | html_utf8          已转换为utf8html
data     | data_urlenc        "url方式编码"的输入数据

核心思想: 如果这是一个需要理解的关键信息, 那么就放到名字里

名字应该有多长

为作用域大的名字采用更长的名字

在小的作用域里可以使用短的名字

输入长名字————不再是个问题(编辑器的"单词补全"功能)

首字母缩略词和缩写

程序员常用: eval 代替 evaluation, doc 代替 document, str 代替 string

使用项目所特有的缩写词非常糟糕.

经验原则是: 团队的新成员是否能理解这个名字的含义? 如果能, 那可能就没有问题.

丢掉没用的词

ConvertToString() -> ToString()
DoServeLoop() -> ServeLoop()

利用名字的格式来传递含义

有目的地使用大小写、下划线

参考《Google开源项目格式规范》

驼峰类名、下划线方法名、常量驼峰加kConstant前缀、宏是全大写下划线分词、构造函数首字母大写、jQuery对象前加$

HTML/CSS中id和class的命名: 用下划线来分开ID中的单词, 用连字符来分开class中的单词.【之前一直纠结于用哪个...因为项目中有的这样用有的那样用...很多人不在乎格式搞得我很纠结啊...】

团队中应该使用统一的规范!!!

第3章 不会误解的名字

要多问自己几遍:"这个名字会被别人解读成其他的含义吗?" —— 主动寻找"误解点"

最好的名字是不会误解的

例子: Filter() —— 二义性词, 不知道其含义是"挑出"还是"减掉""

例子:Clip(text, length) —— 二义性词, 可能是从尾部删除, 也可能时裁掉最大长度为length的一段

max_length也有很多种解读: 字节数、字符数、字数...如果要指"字符数", 可写成max_chars

推荐用minmax来表示(包含)极限【这里还是有点问题, 到底包不包含...书上的例子看起来是不包含】

推荐用firstlast来表示包含的范围【last明显是包含的.】

推荐用beginend来表示包含/排除范围【这对最常用, end在C++标准库中是不包含】

给布尔值命名

当为布尔变量或者返回布尔值的函数选择名字时, 要确保返回truefalse的意义很明确.

通常来讲, 加上像ishascanshould这样的词, 可以把布尔值变得更明确.

bool read_password = true;存在二义性, 修改为bool need_password = true;

避免使用反义的词. 如disable_ssl, 应该用use_ssl更易读.

与使用者的期望匹配

有些名字之所以会让人误解是因为用户对它们的含义有先入为主的印象, 就算你的本意并非如此. 在这种情况下, 最好放弃这个名字而改用一个不会让人误解的名字.

例子: get*() 通常被当做"轻量级访问器", 不应该写需要太大代价的代码.

用户会期望get()或者size()是轻量的方法.

例子: 如何权衡多个备选名字

【应该考虑经验用法, 就是一般这个词是怎么用的】

分析每个备选名字, 考虑各种让人误解的可能性.

第4章 审美

核心思想: 如何使用好的留白, 对齐和顺序来让你的代码变得更容易阅读

让代码变得更易读有三条原则:

  • 使用一致的布局, 让读者很快就习惯这种风格.
  • 让相似的代码看上去相似.
  • 把相关的代码行分组, 形成代码块.

为什么审美这么重要 —— 让人愉悦的代码更容易阅读

重新安排换行带来保持一致和紧凑

如果多个代码块做相似的事情, 尝试让它们有同样的剪影.

用方法来整理不规则的东西

使代码"看上去漂亮"通常会带来不限于表面层次的改进, 它可能会帮你把代码的结构做的更好

在需要时使用列对齐

【这个虽然可使易读, 但是如果使用编辑器的格式化工具, 一般不能保持.】

选一个有意义的顺序, 始终一致地使用它

有一大堆get参数要获取, 我们对代码中每个获取的顺序有一些想法:

  • 让变量的顺序与对应的HTML表单中<input>字段的顺序相匹配.
  • 从"最重要"到"最不重要"排序.
  • 按字母顺序排序.

把声明按块组织起来

把代码分成"段落"【使用空行】

个人风格与一致性

一致的风格比"正确"的风格更重要.

第5章 该写什么样的注释

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

什么不需要注释

不要为那些从代码本身就能快速推断的事实写注释

不要为了注释而注释

不要给不好的名字加注释 —— 应该把名字改好

一个好的名字比一个好的注释更重要, 因为在任何用到这个函数的地方都能看得到它.

好代码 > 坏代码 + 好注释

记录你的思想

写代码时有过重要的想法

加入"导演评论"【我喜欢】

// 出乎意料的是, 对于这些数据用二叉树比哈希表快40%
// 哈希运算的代价比左/右比较大得多

对于为什么代码写成这样而不是那样的内在理由("指导性批注")

为代码中的瑕疵写注释

标记  | 通常的意义
TODO: | 我还没有处理的事情
FIXME:| 已知的无法运行的代码
HACK: | 对一个问题不得不采用的比较粗糙的解决方案
XXX:  | 危险! 这里有重要的问题

你应该可以随时把代码将来应该如何改动的想法用注释记录下来. 这种注释给读者带来对代码质量和当前状态的宝贵见解, 甚至可能会给他们指出如何改进代码的方向.

给常量加注释 —— 【通常很有用. 为什么要定这个值...可不可以修改一类的】

站在读者的角度

想象你的代码对于外人来讲看起来是什么样子的

公布可能的陷阱【未雨绸缪, 预料到人们使用你的代码时可能会遇到的问题, 防止误用】

可能会发生的意料之外的行为.

"全局观"注释

思想下面的场景: 有新人刚刚加入你的团队, 她坐在你旁边, 而你需要让她熟悉代码库.

总结性注释

你可以做任何能帮助读者更容易理解代码的事. 这可能也会包含对于"做什么"、"怎么做"或者"为什么"的注释(或者同时注释这三个方面).

最后的思考 —— 克服"作者心理阻滞"

  1. 不管你心里想什么, 先把它写下来.
  2. 读一下这段注释, 看看有没有什么地方可以改进.
  3. 不断改进.

第6章 写出言简意赅的注释

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

让注释保持紧凑

避免使用不明确的代词

当像"it"和"this"这样的代词可能指代多个事物时, 避免使用它们.

润色粗糙的句子

精确的描述函数的行为

Return the number of lines in this fileCount how many newline bytes('\n') are in the file

用输入/输出例子来说明特别的情况【这种注释比较喜欢】

例子要精心挑选, 可以说明复杂的情况.

// Example: ...

声明代码的意图

高层次意图, 而非明显的细节.

"具名函数参数"的注释

用嵌入的注释来解释难以理解的函数参数

默认参数 Connetc(timeout = 10, user_encryption = false)

做不到, 用Connet(/* timeout_ms= */ 10, /* use_encryption = */ false)

采用信息含量高的词

如果你感觉到一段注释太长了, 那么可以看看是不是可以用一个典型的编程场景来描述它

第二部分 简化循环和逻辑

在程序中定义循环、逻辑和变量, 从而使得代码更容易理解.

试着最小化代码中的"思维包袱"

第7章 把控制流变的易读

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

条件语句中参考的顺序

在写一个比较时, 把改变的值写在左边并且把更稳定的值写在右边更好一些.

if/else语句块的顺序

  • 首先处理正逻辑而不是负逻辑的情况. 例如, 用if(debug)而不是if(!debug).
  • 先处理掉简单的情况. 这种方式可能还会使ifelse在屏幕之内都可见.
  • 先处理有趣的或者是可疑的情况.

即先处理正确的/简单的/有趣的情况. 有时这些准则会冲突, 但是当不冲突时, 这是要遵循的经验法则.

?:条件表达式(又名“三目运算符")

相对于追求最小化代码行数, 一个更好的度量方法是最小化人们理解它所需的时间.

默认情况下都使用if/else. 三目运算符?:只有在最简单的情况下使用

避免do/while循环

C++的开创者Bjarne Stroustrup在《C++程序设计语言》中提到:

我的经验是, do语句是错误和困惑的来源……我倾向于把条件放在"前面我能看到的地方".其结果是, 我倾向避免使用do语句.

【所以Python没有do/while语句吗?】

从函数中提前返回

想要单一出口点的一个动机是保证调用函数结尾的清理代码. 但现代的编程语言为这种保证提供了更精细的方式:

C++          | 析构函数
JavaPython | try finally
Python       | with
C#           | using

臭名昭著的goto

除了C语言之外, 其他语言一般不大需要goto. 应该避免使用goto.

最小化嵌套

嵌套很深的代码很难以理解

当你对代码做改动时, 从全新的角度审视它, 把它作为一个整体来看待

通过提早返回来减少嵌套 —— 多个return语句

减少循环内的嵌套

在循环中, 与提早返回类似的技术是continue(意思是"跳过该项")

if(...) return;在函数中所扮演的保护语句一样, if(...) continue;语句是循环中的保护语句.

你能理解执行的流程吗?

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

不要让代码中使用这些结构的比例太高. 会难以理解, 难以跟踪bug.

第8章 拆分超长的表达式

把你的超长表达式拆分成更容易理解的小块

用做解释的变量

if line.split(':')[0].strip() == "root":
    

拆分为:

username = line.split(':')[0].strip()
if username == "root":
    

引入一个额外的变量 —— "解释变量", 三个好处:

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

总结变量

if(request.user_id == document.owner_id) {
   
}

if条件有太多的变量, 可能需要花点时间来理解. 改为:

final boolean user_owns_document = (request.user_id == document.owner_id)
if(user_owns_document) {
    
}

使用德摩根定理

  1. not (a or b or c) <=> (not a) and (not b) and (not c)
  2. not (a and b and c) <=> (not a) or (not b) or (not c)

"分别取反, 转换与/或"

例如: if(!(a && !b)) 变成 if(!a || b)

滥用短路逻辑

要小心"智能"的小代码段 —— 它们往往在以后会让别人读起来感到困惑

例子: 与复杂的逻辑战斗

struct Range{
    int begin;
    int end;
    //For example, [0,5) overlaps with [3,8)
    bool OverlapsWith(Range other);
};

初始方案

bool Range::OverlapsWith(Range other) {
    return (begin >= other.begin && begin < other.end) ||
           (end > other.begin && end <= other.end) ||
           (begin <= other.begin && end >= other.end);
}

"反方向"解决问题(考虑目标的对立面)

bool Range::OverlapsWith(Range other){
    if(other.end <= begin) return false;
    if(other.begin >= end) return false;
    return true;
}

拆分巨大的语句

提取公用部分放入函数作为总结变量

  • 帮助避免录入错误
  • 进一步缩短行宽度, 使代码更容易快速阅读
  • 如果类的名字需要改变, 只需要改一个地方即可

另一个简化表达式的创意方法

使用宏

第9章 变量与可读性

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

减少变量

没有价值的临时变量

减少中间结果【"速战速决"】

减少控制流变量

缩小变量的作用域

让你的变量对尽量少的代码行可见【这里面举的例子都比较明显】

只写一次的变量更好

操作一个变量的地方越多, 越难确定它的当前值

最后的例子

第三部分 重新组织代码

在更高层次上组织大的代码块以及在功能层次上解决问题的方法

第10章 抽取不相关的子问题

"积极地发现并抽取出不相关的子逻辑"

  • 看看某个函数或者代码块, 问问自己: 这段代码高层次的目标是什么?
  • 对于每一行代码, 问一下: 它是直接为了目标而工作吗? 这段代码高层次的目标是什么呢?
  • 如果足够的行数在解决不相关的子问题, 抽取代码到独立的函数中.

把一般代码和项目专有的代码分开

介绍性的例子:findClosestLocation()

纯工具代码

通常来讲, 如果你在想:"我希望我们的库里有XYZ()函数", 那么就写一个! (如果它还不存在的话) 经过一段时间, 你会建立起一组不错的工具代码.

其他多用途代码

子代码自成一体后改进它变得更容易

创建大量通用代码

通用代码很好, 因为"它完全地从项目的其他部分中解耦出来"

从你的项目中拆分出越多的独立库越多越好, 因为你代码的其他部分会更小而且更容易思考.

项目专有的功能

简化已有接口

"你永远都不要安于使用不理想的接口"

按需重塑接口

过犹不及

别分的太细, 多个小函数对可读性不利

"把一般代码和项目专有的代码分开"

第11章 一次只做一件事

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

流程:

  1. 列出代码所做的所有"任务"
  2. 尽量把这件任务拆分到不同的函数中, 或者至少是代码中不同的段落中.

任务可以很小

投票的例子

从对象中抽取值

更大型的例子

第12章 把想法变成代码

如果你不能把一件事情解释给你祖母听的话说明你还没有真正理解它. —— 阿尔伯特 爱因斯坦

当把一件复杂的事向别人解释时, 那些小细节很容易就会让他们迷惑. 把一个想法用"自然语言"解释是个很有价值的能力, 因为这样其他知识没你这么渊博的人才可以理解它. 这需要把一个想法精炼成最重要的概念. 这样做不仅帮助他人理解, 而且也帮助你自己把这个想法想得更清晰.

清楚地描述逻辑

用自然语言描述逻辑

了解函数库是有帮助的

编写精炼代码的一部分工作是了解你的库提供了什么.

把这个方法应用于更大的问题

自然语言描述逻辑、适当递归调用自身

第13章 少些代码

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

别费神实现那个功能 —— 你不会需要它

质疑和拆分你的需求

保持小代码库

让你的代码库越小, 越轻量级越好

  • 创建越多越好的"工具"代码来减少重复代码
  • 减少无用代码或没有用的功能
  • 让你的项目保持分开的子项目状态
  • 总的来说, 要小心代码的"重量". 让它保持又轻又灵.

删除没用的代码

熟悉你周边的库

每隔一段时间, 花15分钟阅读标准库中的所有函数/模块/类型的名字. 这包括C++标准模板库(STL)、Java API、Python内置的模块以及其他内容.

成熟的库中, 每行代码都代表大量的设计、调试、重写、文档、优化和测试

例子: 使用Unix工具而非编写代码

通过以下方法避免编写新代码:

  • 从项目中消除不必要的功能, 不要过度设计.
  • 重新考虑需求, 解决版本最简单的问题, 只要能完成工作就行.
  • 经常性地通读标准库的整个API, 保持对它们的熟悉程度.

第四部分 精选话题

把"易于理解"的思想应用于测试以及大数据结构代码的例子

第14章 测试与可读性

使测试易于阅读和维护

测试代码的可读性与非测试代码的可读性是同样重要的

测试应当具有可读性, 以便其他程序员可以舒服地改变或者增加测试

这段测试什么地方不对

使这个测试更可读

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

创建最小的测试声明

大多数测试的基本内容都能精炼成"对于这样的输入/输出情形, 期望有这样的行为/输出"

实现定制的"微语言"

让错误消息具有可读性

了解显示错误的库, 更好版本的assert()

手工打造错误消息

错误消息应当越有帮助越好

选择好的测试输入

基本原则是, 你应当选择一组最简单的输入, 它能完整地使用被测代码

又简单又能完成工作的测试值更好

一个功能的多个测试

为测试函数命名

应该能反应:

  1. 被测试的类
  2. 被测试的函数
  3. 被测试的场景或bug

Test_<FunctionName>_<Situation>()

那个测试有什么地方不对

对测试较好地开发方式

TDD

你会开始把代码写得容易测试!

走得太远

  • 牺牲真实代码的可读性, 只是为了使能测试.
  • 着迷于100%的测试覆盖率.
  • 让测试成为产品开发的阻碍.

第15章 设计并改进"分钟/小时计数器"

【这部分涉及到一些数据结构方面的知识】

问题

跟踪过去一分钟和一个小时里Web服务器传输了多少字节

定义类接口

改进命名 -》 改进注释

尝试1: 一个幼稚的方案

只是提取代码中重复的部分

尝试2: 传送带设计方案

尝试3: 时间桶设计方案

比较三种方案

总得来说就是一步步优化, 可以学习一下这章中使用到的数据结构.

附录 深入阅读【一些书】

关于写高质量代码的书

关于各种编程话题的书

历史上重要的书目

相关笔记 —— 下次复习可比较一下