读《Clean Code》

这是一篇旧文,其中的内容可能已经过时。
wtfm
评判代码好坏的唯一标准——每分钟的 WTF 次数

是的,我们就是一群代码猴子,上蹿下跳,自以为领略了编程的真谛。可惜,当我们抓着几个酸桃子,得意洋洋地坐到树枝上,却对自己造成的混乱熟视无睹。那堆“可以运行”的乱麻程序,就在我们眼皮底下慢慢腐坏。

当我时常为写出“短小精炼”的代码而沾沾自喜时,这本书无疑给了我当头一棒,让我下定决心痛改前非。本书第一章举了一些例子来说明糟糕代码能带来多坏的影响。从第二章到第十三章,本书列举了整洁代码应有的一切特性。作者提倡注重细节,“应当用为自己的孩子命名般的谨慎去给变量命名”,正是这种对细节的执着造就了代码的整洁性。“写整洁代码,需要遵循大量的小技巧,来贯彻刻苦习得的‘整洁感’。”同时作者认为,要写出整洁代码,必须进行大量练习,否则“会如同第一次骑车的车手,即使学习了骑车的所有要领,还是会跌倒在地。”

对于软件而言,没有不变的需求,也没有不变的代码,实际上我们会花很多时间来读自己或他人的代码。因此,从软件的第一个版本开始,专注于细节的整洁,并保持代码的良好可读性就变得尤为重要。另外,i+++i这种东西百害而无一利,它们只会把代码变成没人愿意看的一团乱麻。

整洁代码的要领

整洁代码原则上要像散文一样流畅,能自上而下地反映系统的设计。每一行代码,都能准确反映作者的意图。没有重复代码,还要包括尽量少的实体(类、方法、函数等)。

变量命名不怕长,要名副其实,遵循驼峰命名法。用常量代替“幻数(magic number)”,首字母大写。简单举例,

const PI = 3.14;
function getAreaOfCircleByRadius(radius) {
  return radius * radius * PI;
}

就比

function f(a) {
  return a * a * 3.14;
}

要好。

一个函数只做一件事情,并且保持简短。事实上这两个条件相辅相成。大的函数要拆分为小的函数的组合。函数参数不应当多于三个,否则一些参数应当被封装为对象。

作者认为用来表达意图的注释是“不得已的恶行”,不如修整代码本身。如果代码写得足够清爽,是不必有这种注释产生的。作者建议,当想要添加解释型注释时,不如用切分小函数的方式。例如,

function bigFunction() {
  // do something
  /* CODE */
  // do something else
  /* CODE */
}

不如用下面的写法:

function bigFunction() {
  doSomething();
  doSomethingElse();
}
function doSomething() {
  /* CODE */
}
function doSomethingElse() {
  /* CODE */
}

如果有暂时不用的代码,作者强烈呼吁直接删除而不是注释掉。否则过了一两个星期,我们怎么知道这代码还有没有用?不但放着碍眼,想删还不敢删。版本控制系统会记住历史代码,所以放心大胆地删掉无用代码吧。

保持一直的垂直空行、空格和缩进格式。逻辑联系紧密的代码,视觉上也应当紧密,反之亦然。

避免否定运算符出现判断条件中,if(isGood)就比if(!isBad)更容易理解。

单元测试的至高原则是可读性。捕获错误时,应当抛出异常而不是使用错误码。要为每种边界情况编写测试,保持整齐的代码边界。

出现重复的代码意味着抽象不足,应把重复代码的共性抽象为类或方法。

类如同函数,应当只有一个权责。臃肿的类要依据权责切分为小类的组合。

可配置数据要在较高层级上放置,局部变量要出现在第一次使用时的上方。

最重要的一点是,不要把上面当作教条来执行。必须做大量的练习,观察自己的失败或他人的失败,形成自己的“代码感”。本书最后用了三章篇幅,每一章都展示了对一个 Java 项目的逐步改进过程,值得深入体会。

代码规范

几乎所有的流行规范都要比原生语法更加严格,这也印证了“发送时要保守,接受时要开放”的设计原理,或者叫“严于律己,宽以待人”。目前,我主要写 JavaScript 和 php,遵守的是JSLintphp-fig规范。

JSLint 简明地阐述了代码中什么叫“好的部分”:如果一种语言特性有时候很有用然而有时候很糟糕,而且存在一种更好的替换方案,那么永远使用那个更好的方案。尤其是对于 JavaScript 和 php 这种对开发者友好的弱类型脚本语言来说,语法中存在不少弊端,例如一位熟练的开发者都未必能回答出0==null到底返回什么。所以这两种规范都禁止使用==运算符。

Fortran 首先将=作为赋值符号,犯了第一个错误。更糟糕的是,大多数语言把这个错误发扬光大了。C 语言将 ==作为比较运算符,两者视觉上的相似性让它成为第二个错误。JavaScript 的==行为更加古怪。0==''0=='0'''=='0',猜这里有几个返回true?所幸,===的存在一定程度上弥补了这个问题。

规范中有些部分,比如禁止省略大括号,不仅适用于 JavaScript 和 php,而适用于大多数语言规范。从根源上说,越能坚持严格的代码风格,就越能回避语法陷阱。即使跳出编程这件事,这句话也不会错。