编写可读代码的艺术

来源:BeiYuu 的博客

这是《The Art of Readable Code | 编写可读代码的艺术》的读书笔记,再加一点自己的认识,强烈推荐此书。

 

代码为什么要易于理解

“Code should be written to minimize the time it would take for someone else to understand it.”

日常工作的事实是:

  • 写代码前的思考和看代码的时间远大于真正写的时间
  • 读代码是很平常的事情,不论是别人的,还是自己的,半年前写的可认为是别人的代码
  • 代码可读性高,很快就可以理解程序的逻辑,进入工作状态
  • 行数少的代码不一定就容易理解
  • 代码的可读性与程序的效率、架构、易于测试一点也不冲突

整本书都围绕“如何让代码的可读性更高”这个目标来写。这也是好代码的重要标准之一。

如何命名

变量名中应包含更多信息

使用含义明确的词,比如用download而不是get,参考以下替换方案:

避免通用的词

tmpretval这样词,除了说明是临时变量和返回值之外,没有任何意义。但是给他加一些有意义的词,就会很明确:

不使用retval而使用变量真正代表的意义:

嵌套的for循环中,ij也有同样让人困惑的时候:

换一种写法就会清晰很多:

所以,当使用一些通用的词,要有充分的理由才可以。

使用具体的名字

CanListenOnPort就比ServerCanStart好,can start比较含糊,而listen on port确切的说明了这个方法将要做什么。

--run_locally就不如--extra_logging来的明确。

增加重要的细节,比如变量的单位_ms,对原始字符串加_raw

如果一个变量很重要,那么在名字上多加一些额外的字就会更加易读,比如将string id; // Example: "af84ef845cd8"换成string hex_id;

更多例子:

对于作用域大的变量使用较长的名字

在比较小的作用域内,可以使用较短的变量名,在较大的作用域内使用的变量,最好用长一点的名字,编辑器的自动补全都可以很好的减少键盘输入。对于一些缩写前缀,尽量选择众所周知的(如str),一个判断标准是,当新成员加入时,是否可以无需他人帮助而明白前缀代表什么。

合理使用_-等符号,比如对私有变量加_前缀。

命名不能有歧义

命名的时候可以先想一下,我要用的这个词是否有别的含义。举个例子:

现在的结果到底是包含2011年之前的呢还是不包含呢?

使用minmax代替limit

对比上例中CART_TOO_BIG_LIMITMAX_ITEMS_IN_CART,想想哪个更好呢?

使用firstlast来表示闭区间

firstlast含义明确,适宜表示闭区间。

使用beiginend表示前闭后开(2,9))区间

上面一种写法就比下面的舒服多了。

Boolean型变量命名

这是一个很危险的命名,到底是需要读取密码呢,还是密码已经被读取呢,不知道,所以这个变量可以使用user_is_authenticated代替。通常,给Boolean型变量添加ishascanshould可以让含义更清晰,比如:

符合预期

在这个例子中,getMean方法遍历了所有的样本,返回总额,所以并不是普通意义上轻量的get方法,所以应该取名computeMean比较合适。

漂亮的格式

写出来漂亮的格式,充满美感,读起来自然也会舒服很多,对比下面两个例子:

什么是充满美感的呢:

考虑断行的连续性和简洁

这段代码需要断行,来满足不超过一行80个字符的要求,参数也需要注释说明:

考虑到代码的连贯性,先优化成这样:

连贯性好一点,但还是太罗嗦,额外占用很多空间:

用函数封装

上面这段代码看起来很脏乱,很多重复性的东西,可以用函数封装:

列对齐

列对齐可以让代码段看起来更舒适:

代码用块区分

上面这一段虽然能看,不过还有优化空间:

再来看一段代码:

全都混在一起,视觉压力相当大,按功能化块:

让代码看起来更舒服,需要在写的过程中多注意,培养一些好的习惯,尤其当团队合作的时候,代码风格比如大括号的位置并没有对错,但是不遵循团队规范那就是错的。

如何写注释

当你写代码的时候,你会思考很多,但是最终呈现给读者的就只剩代码本身了,额外的信息丢失了,所以注释的目的就是让读者了解更多的信息。

应该注释什么

不应该注释什么

这样的注释毫无价值:

不要像下面这样为了注释而注释:

不要给烂取名注释

注释的大部分都在解释clean是什么意思,那不如换个正确的名字:

记录你的想法

我们讨论了不该注释什么,那么应该注释什么呢?注释应该记录你思考代码怎么写的结果,比如像下面这些:

也可以用来记录流程和常量:

可用的词有:

  • TODO : Stuff I haven’t gotten around to yet
  • FIXME : Known-broken code here
  • HACK : Adimittedly inelegant solution to a problem
  • XXX : Danger! Major problem here

站在读者的角度去思考

当别人读你的代码时,让他们产生疑问的部分,就是你应该注释的地方。

很多C++的程序员啊看到这里,可能会想为什么不用data.clear()来代替vector.swap,所以那个地方应该加上注释:

说明可能陷阱

你在写代码的过程中,可能用到一些hack,或者有其他需要读代码的人知道的陷阱,这时候就应该注释:

而实际上这个发送邮件的函数是调用别的服务,有超时设置,所以需要注释:

全景的注释

有时候为了更清楚说明,需要给整个文件加注释,让读者有个总体的概念:

总结性的注释

即使是在函数内部,也可以有类似文件注释那样的说明注释:

或者按照函数的步进,写一些注释:

很多人不愿意写注释,确实,要写好注释也不是一件简单的事情,也可以在文件专门的地方,留个写注释的区域,可以写下你任何想说的东西。

注释应简明准确

前一个小节讨论了注释应该写什么,这一节来讨论应该怎么写,因为注释很重要,所以要写的精确,注释也占据屏幕空间,所以要简洁。

精简注释

这样写太罗嗦了,尽量精简压缩成这样:

避免有歧义的代词

这里的it's有歧义,不知道所指的是data还是cache,改成如下:

还有更好的解决办法,这里的it就有明确所指:

语句要精简准确

这句话理解起来太费劲,改成如下就好理解很多:

精确描述函数的目的

这样的一个函数,用起来可能会一头雾水,因为他可以有很多歧义:

  • ”” 一个空文件,是0行还是1行?
  • “hello” 只有一行,那么返回值是0还是1?
  • “hello\n” 这种情况返回1还是2?
  • “hello\n world” 返回1还是2?
  • “hello\n\r cruel\n world\r” 返回2、3、4哪一个呢?

所以注释应该这样写:

用实例说明边界情况

这个描述很精确,但是如果再加入一个例子,就更好了:

说明你的代码的真正目的

这里的注释说明了倒序排列,单还不够准确,应该改成这样:

函数调用时的注释

看见这样的一个函数调用,肯定会一头雾水:

如果加上这样的注释,读起来就清楚多了:

使用信息含量丰富的词

上面这一大段注释,解释的很清楚,如果换一个词来代替,也不会有什么疑惑:

简化循环和逻辑

流程控制要简单

让条件语句、循环以及其他控制流程的代码尽可能自然,让读者在阅读过程中不需要停顿思考或者在回头查找,是这一节的目的。

条件语句中参数的位置

对比下面两种条件的写法:

到底是应该按照大于小于的顺序来呢,还是有其他的准则?是的,应该按照参数的意义来

  • 运算符左边:通常是需要被检查的变量,也就是会经常变化的
  • 运算符右边:通常是被比对的样本,一定程度上的常量

这就解释了为什么bytes_received < bytes_expected比反过来更好理解。

if/else的顺序

通常,if/else的顺序你可以自由选择,下面这两种都可以:

或许对此你也没有仔细斟酌过,但在有些时候,一种顺序确实好过另一种:

  • 正向的逻辑在前,比如if(debug)就比if(!debug)
  • 简单逻辑的在前,这样ifelse就可以在一个屏幕显示 – 有趣、清晰的逻辑在前

举个例子来看:

看到if你首先想到的是expand_all,就好像告诉你“不要想大象”,你会忍不住去想它,所以产生了一点点迷惑,最好写成:

三目运算符(?:)

使用三目运算符可以减少代码行数,上例就是一个很好的例证,但是我们的真正目的是减少读代码的时间,所以下面的情况并不适合用三目运算符:

所以只在简单表达式的地方用。

避免使用do/while表达式

这段代码会执行几遍呢,需要时间思考一下,do/while完全可以用别的方法代替,所以应避免使用。

尽早return

函数里面尽早的return,可以让逻辑更加清晰。

减少嵌套

这样一段代码,有一层的嵌套,但是看起来也会稍有迷惑,想想自己的代码,有没有类似的情况呢?可以换个思路去考虑这段代码,并且用尽早return的原则修改,看起来就舒服很多:

同样的,对于有嵌套的循环,可以采用同样的办法:

换一种写法,尽早return,在循环中就用continue:

拆分复杂表达式

很显然的,越复杂的表达式,读起来越费劲,所以应该把那些复杂而庞大的表达式,拆分成一个个易于理解的小式子。

用变量

将复杂表达式拆分最简单的办法,就是增加一个变量:

或者这个例子:

逻辑替换

  • 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)

所以,就可以这样写:

不要滥用逻辑表达式

这样的代码完全可以用下面这个替换,虽然有两行,但是更易懂:

像下面这样的表达式,最好也不要写,因为在有些语言中,x会被赋予第一个为true的变量的值:

拆解大表达式

这里面有很多重复的语句,我们可以用变量还替换简化:

消除变量

前一节,讲到利用变量来拆解大表达式,这一节来讨论如何消除多余的变量。

没用的临时变量

这里的now可以去掉,因为:

  • 并非用来拆分复杂的表达式
  • 也没有增加可读性,因为datetime.datetime.now()本就清晰
  • 只用了一次

所以完全可以写作:

消除条件控制变量

这里的done可以用别的方式更好的完成:

这个例子非常容易修改,如果是比较复杂的嵌套,break可能并不够用,这时候就可以把代码封装到函数中。

减少变量的作用域

我们都听过要避免使用全局变量这样的忠告,是的,当变量的作用域越大,就越难追踪,所以要保持变量小的作用域。

这里的str_的作用域有些大,完全可以换一种方式:

str通过变量函数参数传递,减小了作用域,也更易读。同样的道理也可以用在定义类的时候,将大类拆分成一个个小类。

不要使用嵌套的作用域

这个例子在运行时候会报example_value is undefined的错,修改起来不算难:

但是参考前面的消除中间变量准则,还有更好的办法:

用到了再声明

在C语言中,要求将所有的变量事先声明,这样当用到变量较多时候,读者处理这些信息就会有难度,所以一开始没用到的变量,就暂缓声明:

读者一次处理变量太多,可以暂缓声明:

变量最好只写一次

前面讨论了过多的变量会让读者迷惑,同一个变量,不停的被赋值也会让读者头晕,如果变量变化的次数少一些,代码可读性就更强。

一个例子

假设有一个页面,如下,需要给第一个空的input赋值:

这段代码能工作,有三个变量,我们逐一去看如何优化,found作为中间变量,完全可以消除:

再来看elem变量,只用来做循环,调用了很多次,所以很难跟踪他的值,i也可以用for来修改:

工程师就是将大问题分解为一个个小问题,然后逐个解决,这样也易于保证程序的健壮性、可读性。如何分解子问题,下面给出一些准则:

  • 看看这个方法或代码,问问你自己“这段代码的最终目标是什么?”
  • 对于每一行代码,要问“它与目标直接相关,或者是不相关的子问题?”
  • 如果有足够多行的代码是处理与目标不直接相关的问题,那么抽离成子函数

来看一个例子:

这段代码的目标是发送一个ajax请求,所以其中字符串处理的部分就可以抽离出来:

意外收获

有很多理由将format_pretty抽离出来,这些独立的函数可以很容易的添加feature,增强可靠性,处理边界情况,等等。所以这里,可以将format_pretty增强,就会得到一个更强大的函数:

这个函数输出:

多做这样的事情,就是积累代码的过程,这样的代码可以复用,也可以形成自己的代码库,或者分享给别人。

业务相关的函数

那些与目标不相关函数,抽离出来可以复用,与业务相关的也可以抽出来,保持代码的易读性,例如:

抽离出来,就好看很多:

简化现有接口

我们来看一个读写cookie的函数:

这段代码实在太丑了,理想的接口应该是这样的:

对于并不理想的接口,你永远可以用自己的函数做封装,让接口更好用。

按自己需要写接口

虽然终极目的是拼接用户信息的字符,但是代码大部分做的事情是解析python的object,所以:

这样在其他地方也可以调用:

分离子函数是好习惯,但是也要适度,过度的分离成多个小函数,也会让查找变得困难。

代码应该是一次只完成一个任务

这是一个用来拼地名的函数,有很多的条件判断,读起来非常吃力,有没有办法拆解任务呢?

先拆解第一个任务,将各变量分别保存,这样在后面使用中不需要去记忆那些繁长的key值了,第二个任务,解决地址拼接的后半部分:

再来解决前半部分:

大功告成:

如果注意到有USA这个变量的判断的话,也可以这样写:

要把一个复杂的东西解释给别人,一些细节很容易就让人产生迷惑,所以想象把你的代码用平实的语言解释给别人听,别人是否能懂,有一些准则可以帮助你让代码更清晰:

  • 用最平实的语言描述代码的目的,就像给读者讲述一样
  • 注意描述中关键的字词
  • 让你的代码符合你的描述

下面这段代码用来校验用户的权限:

这一段代码不长,里面的逻辑嵌套倒是复杂,参考前面章节所述,嵌套太多非常影响阅读理解,将这个逻辑用语言描述就是:

根据描述来写代码:

最易懂的代码就是没有代码!

  • 去掉那些没意义的feature,也不要过度设计
  • 重新考虑需求,解决最简单的问题,也能完成整体的目标
  • 熟悉你常用的库,周期性研究他的API

还有一些与测试相关的章节,留给你自己去研读吧,再次推荐此书:

收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部