如果觉得不错,最好去把原书看一遍!
代码应当易于理解
- 关键思想:代码的写法应当使别人理解它所需的时间最小化
- 要把理解代码所需的时间最小化而不是减少代码,比如一条注释尽管增加了代码的长度,但是能让你更快的理解代码
- 先从表面改进:
起好名字
、写好注释
、整洁的代码格式
代码命名
选择专业的词
get
根据情境,用FetchPage()
或者DownloadPage()
代替getPage()
size()
在树中应该用height()
表示高度,numNodes()
表示节点数,用memoryBytes()
表示内存中所占的空间
找到更有表现力的词
单词 | 更多选择 |
---|---|
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
、foo
这样的词 - 某些情况使用空泛的名字也有好处,比如说在交换两个变量的时候使用
tmp
,在循环迭代器中使用i
、j
、iter
,但是在嵌套的循环中,加上有意义的前缀使之更能相互区分
为名字附带更多的信息
如果一个
id
是十六进制的,可以命名为hexId
带单位的值最好附带上单位
函数参数 | 带单位的参数 |
---|---|
start(int delay) | delay –> delay_secs |
createCache(int size) | size –> size_mb |
throttleDownload(float limit) | limit –> max_kbps |
rotate(float angle) | angle –> degree_cw |
- 附带额外信息不仅限于单位,比如其他属性:
情形 | 变量名 | 更好的名字 |
---|---|---|
一个“纯文本”格式的代码,需要加密后才能进一步使用 | password | plaintext_password |
一条用户提供的注释,需要转义之后才能用于显示 | comment | unescaped_comment |
已转化为UTF-8格式的html字节 | html | html_utf8 |
以“url方式编码”的输入数据 | data | data_urlenc |
- 匈牙利表示法把每个变量的“类型信息”都写进名字的前缀中,例如
pLast
代表一个指向最后一个元素的指针(p),个人认为在 java 中不推荐这种命名方式,因为 java 这种强类型的语言在编译期就能确定变量的类型
名字的长度
在小作用域中使用短的名字,相反在大作用域中使用长名字
现代编辑器能方便使你键入长名字
首字母缩略词和单词缩写应该是大家普遍接受和理解的,例如用 doc 代替 document、str 代替 string。
丢掉没用的词,
ConvertToString
可以直接写成toString
,这样也没有丢失任何信息利用名字的格式来传递含义,java 中,例如
情形 | 规则 |
---|---|
类名 | 大驼峰命名法 |
方法、变量名 | 小驼峰命名法 |
常量名 | 全部大写字母,不同单词下划线分割 |
- 不论代码中采用何种规范,团队或者项目要保持一致
不会误解的名字
- 关键思想:要仔细审视名字,“这个名字会被别人误解成其他含义吗”
- 当要定一个值的上线或者下限时,max_和min_是很好的前缀
- 对于包含的范围,使用first和last是很好的选择
- 对于包含/排除的范围,begin和end是常用的选择
- 命名应与使用者的期望相匹配,例如:
get*()
是个“轻量级访问器”,list.size()
应该是一个 O(1) 复杂度的操作 - 很多单词在用来编程时是多义性的,例如 filter、length 和 limit
美观
三条原则
- 使用一致的布局,让读者很快就习惯这种风格
- 让相似的代码看上去相似
- 把相关的代码行分组,形成代码块
美观的重要性
- 整洁的代码能让读者花更少的时间理解代码
提高代码美观
- 重新安排换行来保持一致和紧凑
- 用方法(函数)来整理不规则的代码
- 可以考虑使用列对齐,例如:
#Extract POST parameters to local variables
details = request.POST.get('details')
location = request.POST.get('location')
phone = request.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')
- 选一个有意义的顺序,始终如一的使用它
- 把声明按块组织起来
- 把代码分成有逻辑的段落:把相似的想法放在一起并与其他想法分开
- 一致的风格比正确的风格更重要
注释
关键思想:注释的目的是尽量帮助读者了解得和作者一样多
什么不需要注释
- 不要为了那些从代码本身就能快速推断的事实写注释
- 不要为了注释而注释,没有提供比代码本身更多信息的注释要么删除,要么改进
- 不要给不好的名字加注释—应该把名字改好,好代码>坏代码+好注释
什么需要注释
- 记录你写代码时重要的想法
- 记录对代码有价值的见解,例如:解释代码没法修复的缺陷、代码不整洁的原因
- 为代码中的瑕疵写注释,比如有如下几种标记:
标记 | 通畅的意义 |
---|---|
TODO: | 我还没有处理的事情 |
FIXME: | 已知的无法运行的代码 |
HACK: | 对一个问题不得不采用的比较粗糙的解决方案 |
XXX: | 危险!这里有重要的问题 |
- 给常量加注释,记下决定这个常量值时的想法
站在读者的角度
- 考虑读者阅读这段代码时可能出现提问,并将提问的答案写在注释中
- 公布可能的陷阱
- 编写文件级别的注释,让读者熟悉代码库
- 在一个类或者函数内部编写总结性的注释
克服“作者心里阻滞”,
- 不管心里想什么,先把它写下来
- 读一下这段注释,看看有没有什么可以改进的地方
- 不断改进
当你经常写注释,你会发现步骤1所产生的注释会越来越好,就越不需要后面两步了
写出言简意赅的注释
- 让注释保持紧凑
- 避免使用不明确的代词
- 润色粗糙的句子
- 精确的描述函数的行为
- 对于输入输出,精心挑选一个例子就够了
- 声明代码的意图
- “具名”函数的参数,就是用注释标明函数参数的含义,用来解释难以理解的函数参数
- 采用信息含量高的词
简化循环和逻辑
把控制流变得易读
关键思想:把条件、循环、以及其他对控制流的改变做的越“自然”越好。让读者不用停下来重读代码。
条件语句
- 条件语句中比较参数的顺序,有以下指导原则:
比较的左侧 | 比较的右侧 |
---|---|
“被问询的”表达式,它的值更倾向于不断变化 | 用来做比较的表达式,它的值更倾向于常量 |
这是和日常语言习惯是一致的,我们会很自然的说:“如果你的年收入至少是10万”
“尤达表示法”:在有些语言中(包括C++和C,不包括Java)为了防止
if(obj==NULL)
被写成if(obj=NULL)
,出现了if(NULL==obj)
这样的写法,但是这样不利于理解,与上一条相悖,现代编译器已经能对if(obj=NULL)
给出警告,所以这个写法已经过时了。if/else语句块的顺序:
- 首先处理正逻辑而不是负逻辑的情况。例如,用
if(debug)
而不是if(!debug)
- 先处理掉简单的情况。这种方式可能还会让if和else在屏幕内都可见
- 先处理有趣的或者是可疑的情况
- 首先处理正逻辑而不是负逻辑的情况。例如,用
不要为了减少代码行数而使用三目运算符,它只适用于从两个简单的值中作出选择的情况,例如:
time_str += (hour >= 12) ? "pm" : "am";
因为带有复杂逻辑的三目运算符反而增加了代码的阅读时间
循环
- 避免 do/while 循环,它的 continue 语句会让人迷惑,while 循环相对更加易读。实践中,大多数 do/while 循环都可以写成 while 循环
从函数提前返回
- 函数中使用多条
return
语句是没有问题的 - 实现函数结尾的清理代码的更为精细的方式
语言 | 清理代码的结构化术语 |
---|---|
C++ | 析构函数 |
Java、Python | try finally |
Python | with |
C# | using |
- goto 语句对程序易读性的破坏
嵌套
- 深层次的嵌套严重影响代码的可读性
- 嵌套一开始是很简单的,但是后来的改动会加深嵌套
- 通过提早返回来减少嵌套
- 减少循环内的嵌套,与提早返回类似的技术是使用 continue
代码流程
- 编程语言和库的结构让代码在“幕后运行”,或者让代码难以理解,如:
编程结构 | 高层次程序流程是如何变得不清晰的 |
---|---|
线程 | 不清楚什么时间执行什么代码 |
信号量/中断处理程序 | 有些代码随时有可能执行 |
异常 | 可能会从多个函数调用中向上冒泡一样地执行 |
函数指针和匿名函数 | 很难知道到底会执行什么代码,因为在编译时还没有决定 |
虚方法 | object.virtualMethod() 可能会调用一个未知子类的代码 |
拆分超长表达式
关键思想:把超长表达式拆分成更容易理解的小块
解释变量
- 引入一个额外的变量,使之成为一个小一点的子表达式
总结变量
- 用一个很短的名字来代替一大块代码,会更容易管理和思考
使用德摩根定理
- 分别进行取反、转换与/或,反向操作是提取出“反因子”
不滥用短路操作
- 短路操作虽然可以很智能的运用在某些场景,使之成为条件控制的效果,但是影响代码的理解
- 但短路操作在很多情况下也能达到简洁的目的
拆分巨大的语句
- 复杂的逻辑会产生复杂的表达式,表达式复杂会增加代码的阅读难度,解决它需要转换思维,用更优雅的方式
- 巨大的语句的拆分需要找到重复的部分,进行简化
- 有时需要把问题“反向”或者考虑目标的对立面
变量的可读性
主要问题:
- 变量越多,就越难全部跟踪它们的动向
- 变量的作用域越大,就需要跟踪它的动向越久
- 变量改变得越频繁,就越难以跟踪它的当前值
减少不能改进可读性的变量
- 减少没有价值的临时变量,比如
- 没有拆分任何复杂的表达式
- 没有做更多的解释
- 只用过一次,因此没有压缩任何冗余代码
- 减少中间结果
- 减少控制流变量
while(/*condition*/&&!done){
if(...){
done = true;
continue;
}
}
done就是控制流变量,可以通过更好的运用结构化编程而消除:
while(/*condition*/){
if(...){
break;
}
}
当有多个嵌套的循环时,一个 break
可能不够,通常的解决方案是把代码挪到一个新方法中
缩小变量的作用域
- 避免滥用全局变量
- 让你的代码对尽量少的代码行可见
- 把定义往下移,变量定义在使用之前即可
只写一次的变量更好
- 那些只设置一次值的变量(或者 const、final、常量)使得代码更容易理解
- 就算不能让变量只写一次,让变量在较少的地方改动仍有帮助
- 操作一个变量的地方越多,越难确定它的当前值
重新组织代码
抽取那些与程序主要目的“不相关的子问题”
积极的发现并抽取出不相关的子逻辑
- 理解某个函数或者代码块高层次的目标
- 对于每行代码确定它是否为目标而工作
- 如果有很多代码行在解决 不相关的子问题,将它抽取到独立的函数中
什么是“不相关”的子问题
- 完全是自包含的,并不知道其他程序是如何使用它的
- 纯工具的代码,例如操作字符串、使用哈希表以及读/写文件
- 通用的代码库
优化现有的接口
- 永远不要安于使用不理想的接口
- 创建自己的包装函数和隐藏接口的粗陋细节
- 按需要重塑接口
过犹不及
- 更多的小函数意味着更多的东西需要关注
- 不要为了抽取而抽取
代码应当一次只做一件事情
相当于一个函数应该只做一件事情,但是一个函数也可以用空白行区分不同的事情,来达到逻辑上的清晰
列出任务
- 将所有任务列出来,其中一些任务可以很容易地编程单独的函数(或类)
- 难点在于准确描述列出的所有的小任务
分割任务
- 分开解决任务使代码变得更小
把想法变成代码
方法
- 用自然语言描述程序
- 用这个描述来帮助你写出更自然的代码
用自然语言说事情
- “小黄鸭法”解决所遇到的代码问题
- 如果你不能把问题说明白或者用词语来设计,估计是缺少了什么东西或者什么东西缺少定义
少写代码
- 重用库或者减少功能,可以节省时间并且让代码库保持精简节约
- 最好读的代码就是没有代码
不去费时间实现不需要的功能
- 没用的功能尽管很酷,但会让程序更复杂
质疑和拆分需求
- 从项目中消除不必要的功能,不要过度设计
- 重新考虑需求,解决版本最简单的问题,只要完成工作就行
让你的代码库越小,越轻量级越好
- 创建越多越好的“工具”以减少重复代码
- 减少无用代码或者没有用的功能
- 是项目中的子项目解耦
- 保持代码的轻量级