如果觉得不错,最好去把原书看一遍!

代码应当易于理解

  • 关键思想:代码的写法应当使别人理解它所需的时间最小化
  • 要把理解代码所需的时间最小化而不是减少代码,比如一条注释尽管增加了代码的长度,但是能让你更快的理解代码
  • 先从表面改进:起好名字写好注释整洁的代码格式

代码命名

选择专业的词

  • 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

避免像泛泛的名字

  • 在需要实际意义表现时,避免使用 tmpretvalfoo 这样的词
  • 某些情况使用空泛的名字也有好处,比如说在交换两个变量的时候使用 tmp,在循环迭代器中使用 ijiter,但是在嵌套的循环中,加上有意义的前缀使之更能相互区分

为名字附带更多的信息

  • 如果一个 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、常量)使得代码更容易理解
  • 就算不能让变量只写一次,让变量在较少的地方改动仍有帮助
  • 操作一个变量的地方越多,越难确定它的当前值

重新组织代码

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

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

  • 理解某个函数或者代码块高层次的目标
  • 对于每行代码确定它是否为目标而工作
  • 如果有很多代码行在解决 不相关的子问题,将它抽取到独立的函数中

什么是“不相关”的子问题

  • 完全是自包含的,并不知道其他程序是如何使用它的
  • 纯工具的代码,例如操作字符串、使用哈希表以及读/写文件
  • 通用的代码库

优化现有的接口

  • 永远不要安于使用不理想的接口
  • 创建自己的包装函数和隐藏接口的粗陋细节
  • 按需要重塑接口

过犹不及

  • 更多的小函数意味着更多的东西需要关注
  • 不要为了抽取而抽取

代码应当一次只做一件事情

相当于一个函数应该只做一件事情,但是一个函数也可以用空白行区分不同的事情,来达到逻辑上的清晰

列出任务

  • 将所有任务列出来,其中一些任务可以很容易地编程单独的函数(或类)
  • 难点在于准确描述列出的所有的小任务

分割任务

  • 分开解决任务使代码变得更小

把想法变成代码

方法

  • 用自然语言描述程序
  • 用这个描述来帮助你写出更自然的代码

用自然语言说事情

  • “小黄鸭法”解决所遇到的代码问题
  • 如果你不能把问题说明白或者用词语来设计,估计是缺少了什么东西或者什么东西缺少定义

少写代码

  • 重用库或者减少功能,可以节省时间并且让代码库保持精简节约
  • 最好读的代码就是没有代码

不去费时间实现不需要的功能

  • 没用的功能尽管很酷,但会让程序更复杂

质疑和拆分需求

  • 从项目中消除不必要的功能,不要过度设计
  • 重新考虑需求,解决版本最简单的问题,只要完成工作就行

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

  • 创建越多越好的“工具”以减少重复代码
  • 减少无用代码或者没有用的功能
  • 是项目中的子项目解耦
  • 保持代码的轻量级

保持对标准库的API的熟悉,尽量使用标准库解决现实问题