编写可阅读代码的艺术
1. 代码应易于理解
1.1 可读性的基本标准:
代码的写法应该使别人理解它需要的时间最少1.2 代码短并不是易理解的标准,2者基本没有关系: 如下面2段功能一致的代码。
代码片段1:1
assert(!(bucket = FindBucket(key)) || !bucket -> IsOccupied())
代码片段2:
1
2
3bucket = FindBucket(key);
if (bucket != null)
assert(!bucket -> IsOccupied())
第一部分:表面层次的改进
名称,注释,代码风格
2. 信息装入到名称中
2.1 选择专业的词
1
2
3
4
5GetPage -> FetchPage -> DownloadPage
Class BinaryTree{
int Size();
}
Size() -> NumNodes() -> Height()2.2 找到更有表现力的词 核心思想:清晰和精准比装可爱好
1
2
3
4send -> deliver dispatch route
find -> search locate
start -> launch begin open
make -> create build set up add new2.3 避免使用 tmp retval这种泛泛的名字
在交换算法中,tmp作为临时变量存在还是有意义的!2.4 循环迭代器
i,j,k ,it,iter 一般都可以作为索引或者循环迭代器。
多重循环,可以根据具体需求,修改为 club_i ,member_i,user_i 或者简化为 ci,mi,ui2.5 用具体的名字代替抽象的名字 核心思想: 给变量,函数,类命名的时候,要把它描述得更具体而不是更抽象
例子: –run_locally(本地运行)
本意使程序输出更多的调试信息,但会运行得更慢。一般用于在本机上测试,但当运行在服务器上性能很重要的时候,一般不会使用这个标记;带来的问题:
新成员不明白真正含义,可能认为是在本地运行的标记
偶尔我们需要在远程服务器上查看调试信息,使用这个看上去比较滑稽
有时我们在本地运行性能测试,不需要日志信息,所以不能使用–run_locally
这种情况下,extra_logging这个意义会更好一点2.6 为名字附带更多信息
16进制的id String id -> String hex_id2.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
4if (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 等到底指代什么需要从代码中去获取,最安全的办法就是将这些代词换成明确的词,如 data6.3 润色注释
6.4 精确描述函数的功能
例子:如统计一个文件中的行数 //Return the number of lines in this file 上面没有明确行的定义,是\n,还是\n\r,或者是\r 修改为: //Count how many newline byte(‘\n’) are in the file6.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
避免使用goto7.7 最小化嵌套
1
2
3
4
5
6if(){
if(){
}
}
else{
}避免这种多层嵌套:
1.通过提前返回来减少嵌套
2.减少循环内的嵌套
8. 拆分超长的表达式
核心思想:把你超长的表达式拆分成更容易的小块
8.1 用做解释的变量
1
2if line.split(‘:’)[0].strip() == “root”:
......拆分:
1
2
3username = line.split(‘:’)[0].strip()
if username == “root”:
......- 8.2 总结变量if条件有太多的变量,可能需要花点时间来理解。改为:
1
2
3if(request.user_id == document.owner_id){
……
}1
2
3
4finial boolean user_owns_document = (request.user_id == document.owner_id)
if(user_owns_document){
......
} - 8.3 使用德摩根
1
2not(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 保持小代码库
随着代码库的增加,维护起来难度将大增,所以保持写少量代码是必要的。- 抽出工具代码。(utils),把可以通用的代码整理成通用工具代码。
- 删除不需要的代码或者没用的功能。
- 把项目分割成很多子项目。
- 重视代码的重量,随时意识到要写轻量级的代码。
13.4 熟悉你周边的库
第四部分:精选部分
###14. 测试与可读性
14.1 使测试易于阅读和维护
测试代码的可读性与非测试代码的可读性是同样重要的。14.2 使这个测试更可读
普遍的测试原则:对使用者隐去不重要的点,以便更重要的细节会突出。14.3 让错误的消息具有可读性
更好版本的assert()
手工打造错误消息14.4 选择好的测试输入
核心思想:使用一组简单的输入,它能完整的使用被测代码.
简化输入值
一个功能的多个测试14.5 为测试函数命名
被测试的类
被测试的函数
被测试的场景或bug
不要使用test1,test2这类,一个简单的测试函数名就是将上面这些信息拼凑在一起。可能在加一个Test_
前缀。14.6 对测试比较好的开发方式
TDD14.7 走得太远
牺牲真实代码的可读性,只为使能测试
着迷100%的覆盖率
测试成为产品开发的阻碍
15. 设计并改进“分钟/小时计数器”
一个具体的例子