编写可读代码的艺术

这是我在分享时的讲稿,主要介绍了《编写可读代码的艺术》中关于变量命名的部分
文中大量引用了《编写可读代码的艺术》书中的内容,向原作者以及译者致敬!

大家好, 相信大家都见过这样的代码: 通篇都是单字母变量和魔数, 一眼看过去很难知道它在做什么

POJ-1922-AC源码

这样的

POJ-3176-AC源码

运气好还能看到这样的

第十五届国际混乱代码大赛-获奖作品

想想看, 如果在项目里有 1000 行这样的代码, 维护起来是什么感觉……

在我们的项目中, 我们用了很多办法来增强代码的可读性. 比如, 我们会设定统一的代码格式, 要求为代码添加注释, 在写完代码后更新 Wiki.
同样, 也有很多书在试图让我们的代码更容易理解. 我今天分享的内容, 就是这一系列书籍中的一本 —— 《编写可读代码的艺术》

编写可读代码的艺术

让我们先从最基本的问题开始

什么样的代码才是好代码

这里有两份遍历链表的代码

两份遍历链表的代码

这两份代码, 都是在从头到尾的遍历一份链表, 如果要评判优劣的话, 显然是下边的代码最好, 因为他又短, 又便于理解.

在我们通常的观点中, 一般认为代码是越短越好. 因为代码越短, 所需要理解的元素也就越少, 所以可读性也就越好

但,真的是越短越好吗?

真的是越短越好吗

来看这两段代码.

先看第一段, 在有注释的情况下, 大家能理解这段代码在做些什么吗? 应该很困难

那看第二段, 这样是不是就好一些了. 其实就是在计算 a 乘以 2 的 n 次幂.

第一段代码很紧凑但难于理解, 第二段代码比较长但很容易理解, 如果现在再去评判哪段代码更好的话, 是不是就有点困难了

果真如此?

显然不是.

在编程的世界里, 好的代码, 首先要做到可读性良好. 而对于可读性的度量, 有一种方法, 比其他方法都重要:

对于任何代码, 当我们在写完它之后, 就可以估算一下, 让身边的同事把代码通读一遍并达到理解的水平所需的时间, 这个时间的长短, 就是我们评判代码可读性的尺度.

而这种度量方式, 被称为:『可读性基本定理』:

好的代码, 应该是使别人理解它所需的时间——最小化

好的代码, 应该是别人理解它所需的时间最小化

而且需要特别点出的是, 当我们说『理解』时, 我们对『理解』这个词有着很高的要求.
我们所说的理解, 是指当一个人真的『理解』了这些代码之后, 他应该就能直接去改动它, 找出缺陷并能明白这些代码是怎么和代码的其他部分交互的.
让这个时间最小化, 是评判代码可读性的核心标准.

所以, 如何编写可读代码这个问题就变成了: 『怎样才能编写代码, 让别人理解它所需的时间最小化』

让我们从命名开始.

命名之法: 把信息装进名字里

在为方法、变量命名的时候, 我们要尽量起一个有意义的名字.

比如食人花, 真的很贴切……

贴切的名字: 使用专业的词语

然后来看几条起名时的原则.

首先, 使用专业的词语.

一般来说, 专业的词语总是最有表现力的. 比如在下边这个方法中.

getPage 是一个很模糊的名字, 只看它的名字很难知道它究竟想要做什么.

如果是想从本地的缓存中获取一个页面的话, 应该叫 loadPage

如果是想从数据库中获取一个页面的话, 应该叫 queryPage

如果是想在互联网上抓取一张页面, 那应该叫 fetchPage 或者 downloadPage.

这几个名字, 都比 getPage 更有表现力.

同样, 假定我们有一个二叉树类, 类里有个 size 方法.

显然, 只看方法名也是很难知道它是什么意思.

如果是想知道树的高度的话, 应该用 height

如果是想知道这个二叉树的节点数的话, 应该叫 countNodes

如果是想知道二叉树在内存中所占的空间的话, 应该叫 memSize

这些名字也都比只有一个简单的 size 要好.

然后看这个. Thread 类 里的 stop 方法. 这个方法看起来就很不错了. 简洁明了, 一搭眼就能知道它在做什么.

但, 还是有改进空间.

比如说, 如果这是一个重量级操作, 停止之后就不可以再恢复, 那它应该叫 kill

如果还有方法可以继续这个线程, 那它应该叫 pause

这样就贴切多了.

贴切的名字: 找到更有表现力的词

然后继续.

在中文环境中, 如果我们想要去拿一个东西的话, 可以用『拿』、『取』、『递』、『抓』这些同义词.
在不同语境选择不同的词汇可以让文章更有表现力.
同样, 英文里有很多同义词, 如果能记住这些词, 在写代码的时候也可以让方法的含义变得更直观.

比如表格里的这些词语.

当然, 过分了就不好了.

贴切的名字: 过犹不及

比如 PHP 里有一个 explode 函数, 这个函数的名字很形象, 一看就知道是要把字符串炸碎成块.
但问题是, PHP5.3 之前还有一个内置的函数叫 split. 如果不看说明的话, 根本就不知道这两个函数有什么区别.
这就很尴尬了……

不过补充一下, split 方法从 5.3 起开始被声明为废弃函数, 在 PHP7 里正式移除. 也算是比较好的结果了.

贴切的名字: 避免空泛的名字_1

然后.

在我们平常写循环的时候, 经常会用ijk这样没有意义的名字做循环变量. 但这样往往就需要让读者去回看上下文才能明白变量的内容, 延长了理解所需的时间, 是一项不太好的习惯.

而且, 有时候还会出现问题.

比如, 看这段代码. 在这段代码的最后, members 和 users 使用了错误的索引, 但因为使用了无意义变量, 所以即使是知道用错了, 也很难看出来错在了那儿. 这在后期维护的时候就是一个大坑.

如果换成有意义的名字就好了.

贴切的名字: 避免空泛的名字_2

一目了然.

然后下一条, 在变量名中展示信息.

贴切的名字: 在变量中展示信息

如果一个信息非常重要的话, 我们应该考虑把它嵌到变量名里.

比如, start 方法需要一个延迟启动参数, 我们可以在后边附上 secsond, 来说明是按秒来进行延迟启动

createCache 方法需要设定 size 大小, 如果没有单位的话很难知道这个大小是 b, 还是 kb, 还是 mb,所以可以附上单位 mb, 一目了然.

throttleDownload 也一样, 把 limit 换成 max_kbps, 一下就能知道这是要将最大网速限制为指定 kb 每秒

同样, rotate 是一个旋转操作, 但只看参数的话不知道是顺时针还是逆时针, 也不知道旋转是按角度旋转还是按弧度旋转.
通过把 angle 改成 dgrees_cw, 一下就说明了这是要顺时针旋转 degrees 度.

不过这里要特别说一下, 顺时针在英文里的缩写是 cw, 但在中文世界中很少有人知道这个缩写, 所以作者在这里用反而会导致理解时间变长.

一般来说, 在平常写代码的时候要尽量减少不常见缩写的使用.
如果缩写不能让刚加入项目的新人明白是什么意思的话, 就不要让他出现在代码里.

贴切的名字: 在变量中展示信息

继续, 和上边一样, 这次是把单位换成了信息.

如果是纯文本密码的话, 最好在前边加上 plaintext 说明

如果是需要转义的注释, 可以在前边加上 unescaped 前缀

在 Python 里的字符串变量经常会有编码问题, 所以如果是 html 字符串的话可以考虑加上 utf8 后缀

当然最后一条也是一样. 加个 url_encode 后缀, 理解速度会快很多

贴切的名字: 丢掉没用的词

当然, 加必要信息也不是什么都往里边加. 如果变量名里有没用的单词的话, 完全可以直接拿掉.

比如, coverToString 不如直接用 toString.

同样, serveLoop 和 doServeLoop 一样清楚.

减少冗余信息是一种美德.

然后是, 让变量名不会被误解.

贴切的名字: 让变量名不会误解

假如我们有一个这样的函数(clip)

显然, 只看 clip 这个名字, 它可能会有两种行为:

1
2
3
1.从尾部删掉 length 的长度

2.截取最大长度为 length 的一段

第二种可能性的概率最大, 但只看函数名的话, 没办法完全肯定.

与其让读者乱猜, 不如直接把函数名称改成 truncate, 直接就是截掉的意思, 简单明了.

参数名 length 也不好, 不如直接改成 max_length

但 max_length 还不够好, 因为 length 也有多种解读:
length 可能是字节数, 也可能是字符数, 字数也有可能. 如果只有一个孤零零的 length 的话, 读者还是没法判断到底以什么为单位去截取字符串.

所以, 这就是前面所说的需要把单位附在名字后边的那种情况. 在这里, 我们假定是按字符数截取文本, 所以, 应该用 max_chars, 而不是 max_length

在分页展示数据的时候, 我们经常会遇到为范围变量命名的问题, 这里有几个通用的命名原则

贴切的名字: 描述范围时的通用命名原则

首先, 我们可以使用 min 和 max 来表达包含极限.

在需要表达极限含义的时候, 我们可能会用到 limit 这个词.
但 limit 有少于和少于且包括这两种状态, 不符合清晰明了的原则.
所以命名极限最清楚的方式还是在限制前加上 min 和 max.

同样, 在表达一段区间时, 可以用 first/last 表示包含的含义. 比如这个, print_number 从 0 开始, 到 9 结束

同样, 我们可以用 begin/end 表示 包含/排除 范围, 就像这张图中所展示的一样

在为布尔值进行命名时, 也有一些原则

贴切的名字: 为布尔值命名

对于那些返回布尔类型的函数, 要确保他们返回truefalse的含义非常明确才可以.

比如这个变量, read_password = true, 这就有两个含义: 已经读取过密码, 或者需要读取密码. 在实际看代码的时候就会很困惑.

通常来说, 在布尔值前面要加上is, has, can或者should这样的定语, 可以让变量含义变得更明确

另外一点就是尽量避免用反义名字.
用反义名字会明显的增加我们理解代码时的负担.

比如这个, disable_ssl = false, 这种变量名出现在代码里简直就是反人类……

换成use_ssl = true就好多了.

贴切的名字: 符合用户的预期

命名的最后一条要求就是: 命名时一定要符合用户的预期. 如果 is_mobile 方法实际调用的是 is_url, 绝对会出事……

比如 C++ 里的链表类有个经典的 size 方法.
在 C++11 之前的标准库里, 所有的 size 方法复杂度都是 o(1), 唯独链表的 size() 复杂度是 o(n) 操作.
但是很多人都不知道啊, 于是他们就直接在循环里直接调 size 方法, 然后表现就是程序的复杂度变成了 o(n²) , 所有测试都能跑过但就是慢的出奇, debug 还 de 不出来错误.
群众反响强烈……

当然, 坚持不懈的坑了大家 10 年之后, C++ 终于在 11 年把这个方法改成 o(1) 的了.

讲完变量命名, 来讲一下程序中的控制流

简化控制流: 最小化代码中的思想包袱

在编写程序的时候, 如果没有条件判断和循环的话, 整个代码还是相对比较好看的.
但一旦加上了控制语句, 每多一层if/else, 结构就会复杂一倍.
如果控制语句一直堆叠下去的话, 整个代码就会像漫画里的蛇那样. 可读性…… 几乎为 0

然后这里是简化控制流的几个通用原则

简化控制流: 通用原则

比如, 调整if/else的顺序, 先处理正逻辑, 先处理简单情况, 或者先处理有趣或者可疑的情况.

然后就是最小化嵌套. 这个很好理解, 因为对我们来说, 每层嵌套都是在为我们的“思维栈”加一个条件,
当嵌套很深时, 代码会非常难以理解.

对于这种情况我们可以通过使用提前返回的方式来减少嵌套. 比如处理问题前先判断参数是否正确, 如果存在问题直接报错返回不再向下运行.
像这种提前返回的语句被称为“卫语句”, 我们可以通过卫语句来有效的减少嵌套.

最后就是尽量避免使用三目运算符.
因为所有的三目运算符其实都可以被转换为if/else语句,
而且跟同样的if/else相比, 三目运算符除了节约代码行数之外并没有其他优势, 而且在大部分情况下都会让代码变得更加难以理解.
所以, 如果没有特别的理由, 可以尽量避免使用三目运算符.

拆分超长的表达式: 拆成容易理解的小块

除了 n 层嵌套的循环之外, 另一个很折磨人的就是那些一大坨一大坨的代码块了.
这里介绍几个把他们拆分成容易理解的小块代码的方法.

拆分超长的表达式: 使用解释变量

首先, 是使用解释变量.

比如我们可以用变量名去解释子表达式的含义.

先看这行代码.
如果没有注释帮助的话, 理解代码的功能恐怕要花上一段时间.

但加一个中间变量就好多了.

或者, 我们也可以用总结变量来解释一大块代码.

比如这里的request.user.id == document.owner_id, 这行代码很长, 而且出现了两次. 但它实际上只是要判断一下当前用户是不是文档的所有者而已.
所以我们可以用一个总结变量把这个值记下来.
这样代码也好多了.

拆分超长的表达式: 德摩根定理

另外一点要说的就是德摩根定理, 这个在我们简化条件判断的时候很有用.

只有一句话: 分别取反, 转换与或.

就像下边这样.

重新组织代码: 零散Tips_0

最后是一些零散的建议.

重新组织代码: 零散Tips_1

比如, 如果我们在两个地方用到了同一处代码, 就可以考虑把代码独立出来, 做成函数, 而不是用复制粘贴的形式.

再比如, 如果有可能的话, 每一个函数应该只完成一个功能. 即使不能做到这么小的粒度, 也要尽量把代码按功能拆分到不同的段落中

然后就是当我们编写代码之前, 可以先试着用自然语言把逻辑或者问题描述一遍. 这样可以让代码写的更自然.

另外代码里尽量不要出现被注释掉的代码. 在有版本控制系统的情况下, 应该用代码库来记录代码, 而不是把代码记到注释里.
无用的代码应该直接删除, 不应该留下来影响阅读.

重新组织代码: 零散Tips_2

我们每隔一段时间应该去看下代码库里的函数, 不是为了记下来, 只是去看看有什么可以直接拿来用的代码, 避免重复造轮子.

然后, 对于错误消息, 我们也要尽量把失败消息放在返回值里或者打印出来, 而不是直接丢掉. 在有错误消息的情况下, 会让 debug 工作简单很多.

最后, 过犹不及. 上边说的这些, 其实都是建议. 真正在做的时候, 还是要根据具体情况具体对待, 避免出现过度优化的情况.

我的分享就到这里了, 谢谢大家!

谢谢大家


编写可读代码的艺术
https://www.yaozeyuan.online/2016/06/26/2016/编写可读代码的艺术/
作者
姚泽源
发布于
2016年6月26日
许可协议