The Art of Readable Code

编写可阅读代码的艺术

1. 代码应易于理解

  • 1.1 可读性的基本标准:
    代码的写法应该使别人理解它需要的时间最少

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

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

    代码片段2:

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

第一部分:表面层次的改进

名称,注释,代码风格

2. 信息装入到名称中

  • 2.1 选择专业的词

    1
    2
    3
    4
    5
    GetPage -> FetchPage -> DownloadPage 
    Class BinaryTree{
    int Size();
    }
    Size() -> NumNodes() -> Height()
  • 2.2 找到更有表现力的词 核心思想:清晰和精准比装可爱好

    1
    2
    3
    4
    send  -> deliver dispatch route  
    find -> search locate
    start -> launch begin open
    make -> create build set up add new
  • 2.3 避免使用 tmp retval这种泛泛的名字
    在交换算法中,tmp作为临时变量存在还是有意义的!

  • 2.4 循环迭代器
    i,j,k ,it,iter 一般都可以作为索引或者循环迭代器。
    多重循环,可以根据具体需求,修改为 club_i ,member_i,user_i 或者简化为 ci,mi,ui

  • 2.5 用具体的名字代替抽象的名字 核心思想: 给变量,函数,类命名的时候,要把它描述得更具体而不是更抽象
    例子: –run_locally(本地运行)
    本意使程序输出更多的调试信息,但会运行得更慢。一般用于在本机上测试,但当运行在服务器上性能很重要的时候,一般不会使用这个标记;

    带来的问题:
    新成员不明白真正含义,可能认为是在本地运行的标记
    偶尔我们需要在远程服务器上查看调试信息,使用这个看上去比较滑稽
    有时我们在本地运行性能测试,不需要日志信息,所以不能使用–run_locally
    这种情况下,extra_logging这个意义会更好一点

  • 2.6 为名字附带更多信息
    16进制的id String id -> String hex_id

  • 2.7 带单位的值
    如果是有度量的,最好是带上单位值:var start -> var start_ms = (new Date()).getTime()

  • 2.8 附带其他重要属性
    纯文本格式密码 password -> plaintext_password 已转换为utf8的html html -> html_utf8 核心思想:如果这个一个需要理解的关键信息,那么就放到名字里

  • 2.9 名字应该有多长
    核心思想:在选择好名字的时候,一个隐含的约束就是,名字不能太长

  • 2.10 在小的作用域里可以使用短的名字
    作用域小的标示符(对多少行其它代码可见)不用带上过多的信息:

    1
    2
    3
    4
    if (debug){
    Map<String,int> m;
    LookUpNameNumber(&m);
    }

    m 虽然没有包含更多的信息,但是读者已经掌握理解这段代码的所有信息。

  • 2.11 输入长名称,已经不是问题
    基本上,常用的编辑器都已经有自动补全的功能,输入已经不是什么问题了。

  • 2.12 首字母缩略词和缩写
    程序员常用:eval 代替 evaluation, doc 代替document ,str 代替 string 使用项目缩写开头不是一个好的方式,BEManager,对新员工会有误解。

  • 2.13 丢掉没有用处的词
    ConvertToString -> ToString()

  • 2.14 利用名字的格式来传递信息
    比如遵循一些规范,类名 开头大写,变量 小写开头……
    每种语言不一样,如C++有Google开源开发规范

  • 2.15 其它格式规范
    由公司或者团队所做出的一些开发规范或者约定。

    总结:
    核心就是把信息塞进名子里,让读者通过名字就能获取大量的信息。

3. 不会误解的名字

  • 3.1 推荐用max,min来(包含)极限

  • 3.2 推荐用first,last来表示包含的范围

  • 3.3 推荐用begin,end来表示包含/排除范围

  • 3.4 布尔值命名
    通常加上has,is,can,should这样的词,可以把布尔值变得明确。
    bool read_password = true; 存在二义性。修改为: bool need_password = true;

  • 3.5 与使用者的期望相匹配
    不要使用那种让大家有先入为主的名字。
    如,java中,get是一个轻量级的访问器,返回内部成员变量。如果你的代码中需要一个方法遍历所有经过的数据,并同时计算值的方法getMean()显然会 让大家误解,所以最好的方式是用computerMean()来表示

    总结:
    核心就是不会误解的名字是最好的名字。小心有歧义的名字。

4. 审美

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

  • 4.1 重新安排换行保持一致和紧凑

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

  • 4.3 在需要时使用对其列
    整齐的列可以很方便阅读

  • 4.4 将声明用块组装起来
    最好按逻辑分组,比如说功能块分组

  • 4.5 把代码分成段落

  • 4.6 个人风格与一致性
    一致的风格比正确的风格更重要

5. 该写什么样的注释

核心思想: 不要为那些从代码本身就能推断的事实写注释

  • 5.1 不要为了注释而注释
    函数的声明与其注释时一致的,这类注释要删除或者改进它(增加更多的细节)

  • 5.2 不要给不好的名字加注释---应该把名字改好
    好代码 > 坏代码 + 注释

  • 5.3 记录你的思想

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

  • 5.3.2 为代码的瑕疵写注释
    当代码需要改进:
    //todo: 采用更快算法
    标记 通常的意义
    todo 我还没处理完的事情
    fix me 已知的无法运行的代码
    hack 对一个问题不得不采用比较粗糙的解决方案
    xxx 危险,这里有重要的问题

  • 5.3.3 给常量加注释

  • 5.4 站在读者的角度
    意料之中的提问
    公布可能的陷进
    全局观注释
    总结性注释

  • 5.5 克服”作者心里阻滞”
    不管想什么,先写下来
    读一下注释,看看有什么地方需要改进
    不断改进

6 写出言简意赅的注释

核心思想: 注释应该有更高的 信息/空间 率

  • 6.1 让注释保持紧凑

  • 6.2 避免使用不明确的代词
    如 it,this 等到底指代什么需要从代码中去获取,最安全的办法就是将这些代词换成明确的词,如 data

  • 6.3 润色注释

  • 6.4 精确描述函数的功能
    例子:如统计一个文件中的行数 //Return the number of lines in this file 上面没有明确行的定义,是\n,还是\n\r,或者是\r 修改为: //Count how many newline byte(‘\n’) are in the file

  • 6.5 用输入/输出的例子来说明特别的情况

  • 6.6 声明代码的用途

  • 6.7 “具名函数参数”的注释

  • 6.8 采用信息含量高的词
    当你发现注释非常长的时候,就得考虑是否可以用一个编程场景来描述它。

第二部分 简化循环和逻辑

7. 把控制流变得易读

核心思想: 把条件,循环及其他对控制流的改变做得越自然越好,运行一种方法让读者不用停下来就能重读你的代码

  • 7.1 条件语句中参数的顺序
    比较的左侧,被询问的表达式,它的值更倾向于不断变化。if (length >= 10) 优于 if(10 < length)
    比较的右侧,用来做比较的表达式,它的值更倾向于常量。while(received < expected) 优于 while(expected > received)

  • 7.2 if/else 语句块的顺序
    1.首先处理正逻辑,而不是负逻辑。 if(debug)而不是 if(!debug)
    2.先处理简单的逻辑
    3.先处理有趣或者可疑的逻辑

  • 7.3 ?: 条件表达式
    简单的逻辑可以用三目运算符,复杂的逻辑可以换为if/else来处理

  • 7.4 避免do/while循环

  • 7.5 从函数中提前返回

  • 7.6 臭名昭著的goto
    避免使用goto

  • 7.7 最小化嵌套

    1
    2
    3
    4
    5
    6
    if(){
    if(){
    }
    }
    else{
    }

    避免这种多层嵌套:
    1.通过提前返回来减少嵌套
    2.减少循环内的嵌套

8. 拆分超长的表达式

核心思想:把你超长的表达式拆分成更容易的小块

  • 8.1 用做解释的变量

    1
    2
    if line.split(‘:’)[0].strip() == “root”:
    ......

    拆分:

    1
    2
    3
    username = line.split(‘:’)[0].strip()
    if username == “root”:
    ......
  • 8.2 总结变量
    1
    2
    3
    if(request.user_id == document.owner_id){
    ……
    }
    if条件有太多的变量,可能需要花点时间来理解。改为:
    1
    2
    3
    4
    finial boolean user_owns_document = (request.user_id == document.owner_id)
    if(user_owns_document){
    ......
    }
  • 8.3 使用德摩根
    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)
  • 8.4 滥用短路逻辑
    小心“智能”的小段代码

  • 8.5 拆分巨大的语句

9. 变量和可读性

  • 9.1 减少变量
    没有价值的临时变量
    减少控制流变量

  • 9.2 缩小变量的作用域
    让你的变量对尽量少的代码行可见

  • 9.3 只写一次的变量更好
    操作一个变量的地方越多,越来确定它的当前值。

第三部分 重新组织代码

10. 抽取不相关的子问题

工程学就是把大问题拆分成小问题,再把这些问题的解决方案放在一起。本章的建议,积极的发现和抽取出不相关的子逻辑。

  • 10.1 纯工具代码
    有一组核心功能,大多数程序都会用。如操作字符串,文件读写类

  • 10.2 其它多用途代码

  • 10.3 创建大量通用代码

  • 10.4 项目专有的功能

  • 10.5 简化已有的接口
    简单而且强大

  • 10.6 按需重构接口

  • 10.7 过犹不及 (不要盲目的不断抽取)
    总结:把一般代码和项目专有代码分开

11. 一次只做一件事

核心思想:应该把代码组织得一次只做一件事情。本章是给代码做整理碎片的工作

  • 11.1 任务可以很小

  • 11.2 从对象中抽取值

12. 把想法变成代码

  • 12.1 清楚的描述逻辑,可以用自然语言描述

  • 12.2 了解函数库是有帮助的

  • 12.3 把这个方法应用于更大的问题
    用自然语言描述解决方案
    递归的使用这种方法

13. 少写代码

核心思想:最好读的代码就是没有代码

  • 13.1 别费神实现那个功能–你不会需要它

  • 13.2 质疑和拆分你的需求
    仔细检查你的需求,把它消减成一个简单的问题,用很少的代码来实现。

  • 13.3 保持小代码库
    随着代码库的增加,维护起来难度将大增,所以保持写少量代码是必要的。

    1. 抽出工具代码。(utils),把可以通用的代码整理成通用工具代码。
    2. 删除不需要的代码或者没用的功能。
    3. 把项目分割成很多子项目。
    4. 重视代码的重量,随时意识到要写轻量级的代码。
  • 13.4 熟悉你周边的库

第四部分:精选部分

###14. 测试与可读性

  • 14.1 使测试易于阅读和维护
    测试代码的可读性与非测试代码的可读性是同样重要的。

  • 14.2 使这个测试更可读
    普遍的测试原则:对使用者隐去不重要的点,以便更重要的细节会突出。

  • 14.3 让错误的消息具有可读性
    更好版本的assert()
    手工打造错误消息

  • 14.4 选择好的测试输入
    核心思想:使用一组简单的输入,它能完整的使用被测代码.
    简化输入值
    一个功能的多个测试

  • 14.5 为测试函数命名
    被测试的类
    被测试的函数
    被测试的场景或bug
    不要使用test1,test2这类,一个简单的测试函数名就是将上面这些信息拼凑在一起。可能在加一个Test_前缀。

  • 14.6 对测试比较好的开发方式
    TDD

  • 14.7 走得太远
    牺牲真实代码的可读性,只为使能测试
    着迷100%的覆盖率
    测试成为产品开发的阻碍

15. 设计并改进“分钟/小时计数器”

一个具体的例子