这是我参与更文挑战的第16天,活动详情查看:更文挑战
本文为阅读《编写可读代码的艺术》后整理的重点笔记,其中很多观点既实用又清晰,对日常代码良好习惯的养成很有帮助。
首先我们需要明确,什么是「可读性」?作者认为可读性的基本定理是:
代码的写法应当使别人理解它所需的时间最小化。
即使一个项目中不存在「别人」,只有开发者自己,但这个「别人」可能就是六个月后的自己。我想很多人都有过隔段时间回头看自己的代码,就已经觉得很陌生了。而且我们永远也不知道,以后会不会有其他人加入项目。
所以编写有可读性的代码,是必备的基本功,也是程序员的良好职业素养。
(P.S. 书中的部分例子本文改写成了 JavaScript)
一、表面层次的改进
1. 把信息装到名字里
-
选择专业/更有表现力的词
| 单词 | 更多选择 |
|---|---|
| Get | Fetch, Download |
| Stop | Kill, Pause |
| Send | Deliver, Dispatch, Announce, Distribute, Route |
| Find | Search, Extract, Locate, Recover |
| Start | Launch, Create, Begin, Open |
| Make | Create, SetUp, Build, Generate, Compose, Add, New |
-
避免使用像
tmp和retval这样泛泛的名字不要滥用
tmp,它只应该用于短期存在、提供临时存储的情况:
if (right > left) {
tmp = right;
right = left;
left = tmp;
}
retval含义太空泛,应该选择更确切的名称:
// 👎 我是一个返回值
retval += v[i] * v[i];
// 👍 我是一个平方和
sumSquares += v[i] * v[i];
-
更精确的循环迭代器命名
通常我们为了简洁,在循环时会用
ijk来给迭代器命名,但在一些比较复杂的循环中,更精确的命名会更清晰,并且容易发现错误。
for(let i = 0; i < orders.length; i++)
for(let j = 0; j < orders[i].items.length; j++)
for(let k = 0; k < services.length; k++)
if (orders[i].items[k].services == services[j])
在这个例子里,最后把j和k写错了,但不容易一眼找到问题。如果我们用oi(orders_i)、ii(items_i)、si(services_i)来命名,可读性会更强,也不容易出错。
for (left oi = 0; oi < orders.length; oi++)
for(let ii = 0; ii < orders[oi].items.length; ii++)
for(let si = 0; si < services.length; si++)
if (orders[oi].items[si].services == services[ii])
-
为名字附带更多信息
var start = (new Date()).getTime();
...
var elapsed = (new Date()).getTime() - start;
document.writeIn("Load time was: " + elapsed + " seconds";
这是一个计算页面加载时间的例子,因为没有带单位,导致容易把秒和毫秒弄错。在命名时加上单位就能避免这个问题:
var start_ms = (new Date()).getTime();
...
var elapsed_ms = (new Date()).getTime() - start_ms;
document.writeIn("Load time was: " + elapsed_ms / 1000 + " seconds";
-
名字应该有多长
有时候为了表达清楚含义,会把命名写得很长,但如果长到这种程度可能会很头疼:
newNavigationControllerWrappingViewContrllerForDataSourceOfClass
再例如一个变量,可以有以下三种命名:
ddaysdaysSinceLastUpdate
哪一种长度才是合适的呢?
-
在小的作用域里可以使用短的名字,大的作用域最好使用包含足够信息的名字,否则含义会很不清楚。
例如: 作用域在10行内的变量,可以清晰看到它的类型、初值等,用简单的命名也不会产生误解。但如果是全局变量就会理解困难。
-
少使用不熟悉的缩写,原则可以参考:团队的新成员是否能理解这个名字的含义。
例如:
doc代替document容易理解,BEManager代替BackEndManager可能有点困难。 -
当驼峰命名中的单元超过了3个之后,就会很影响阅读体验
例如:
userFriendsInfoModelmemoryCacheCalculateTool -
丢掉没用的词:有时名字中的某些单词可以拿掉而不会损失任何信息。
例如:
convertToString可以省略为toString -
有目的地使用大小写、下划线等
例如:
$Content代表Jquery变量,_getLast()表示私有属性不可被外部调用,CONSTANT_NAME表示常量
2. 不会误解的名字
怎样才能取一个不会被误解的名字呢?
- 要多问自己几遍:“这个名字会被别人解读成其他的意义吗?”
- 推荐用
min和max来表示(包含)极限,优于使用limit - 推荐用
first和last来表示包含的范围,优于使用start和stop - 推荐用
begin和end来表示包含/排除范围 - 使用
is、has、can这样的词来明确表示它是布尔值,避免使用反义的词 - 与使用者的期望相匹配,例如
get通常被认为是轻量级访问器
3. 审美
在保持代码的美感方面,也需要注意一些地方。
三条原则
- 使用一致的布局,让读者很快就习惯这种风格
- 让相似的代码看上去相似
- 把相关的代码行分组,形成代码块
方法
- 重新安排换行来保持一致和紧凑
- 用方法来整理不规则的东西
- 在需要时使用列对齐
nameLabel.text = model.name;
sexLabel.text = model.sex;
addressLabel.text = model.address;
-
选一个有意义的顺序,始终一致的使用它:
- 让变量的顺序与对应的HTML表单中字段的顺序相匹配
- 从“最重要”到“最不重要”排序
- 按字母顺序排序
-
把声明按块组织起来,把代码分成“段落”
BigFunction {
// step1: xxx
...
// step2: xxx
...
// step3: xxx
...
}
- 个人风格与一致性: 一致的风格比“正确”的风格更重要
4. 该写什么样的注释
关键思想: 注释的目的是尽量帮助读者了解得和作者一样多。
什么不需要注释
- 不要为那些从代码本身就能快速推断的事实写注释
- 不要为了注释而注释
- 不要给不好的名字加注释(拐杖式注释)—— 应该把名字改好(好代码 > 坏代码 + 好注释)
什么应该作为注释
-
记录你对代码有价值的见解
很多时候代码本身却无法将这些思考表达出来,所以你就可能有必要通过注释的方式来呈现你的思考,让阅读代码的人知道这段代码是哪些思考的结晶,从而也让读者理解了这段代码为什么这么写。
| 标记 | 通常的含义 |
|---|---|
| TODO: | 待处理的事情 |
| FIXME: | 已知的有问题的代码 |
| HACK: | 对一个问题采取的粗糙的临时方案 |
| XXX: | 危险!这里有重要的问题 |
-
为代码中的瑕疵写注释
临时方案,写出缺陷,暂时没有想到替代方案,或者想到了却因为时间或能力无法实现。
// 该方案有一个很容易忽略的陷阱: xxx
// 该方案存在性能瓶颈,在xx函数中
// 该方案的性能可能并不是最好的,如果使用某某算法可能会好很多
- 给常量加注释
image_quality = 0.72; // 最佳的size / quanlity 比率
retry_limit = 4; // 服务器性能所允许的请求重试上限
-
「全局观」注释:
比如组织架构,类与类之间如何交互,数据如何保存,如何流动,以及模块的入口点等等。可以想象成带新人熟悉代码的场景。
// 这个文件包含了一些辅助函数,为某项业务提供了便利的接口
注释的规范(详见Airbnb规范)
- 让注释保持紧凑,精确的描述函数的行为
- 用
输入 / 输出例子来说明特别的情况 - 使用
/** ... */作为多行注释。包含描述、指定所有参数和返回值的类型和值。
二、简化循环和逻辑
1. 把控制流变得易读
把条件、循环以及其他对控制流的改变做得越“自然”越好
运用一种方式使读者不用停下来重读你的代码
条件语句中参数的顺序
对于条件语句中参数的先后顺序,有以下几种情况:
// code 1
if(length > 10)
// code 2
if(10 < length)
// code 3
if(received_number < standard_number)
// code 4
if(standard_number < received_number)
更推荐使用第一种和第三种顺序,原则是:
- 比较式的左侧: 通常放「被询问的」表达式,它的值一般是在不断变化的;
- 比较式的右侧: 通常是用来「做比较」的表达式,它的值更倾向于常量。
if/else 语句块的顺序
- 首先处理正逻辑而不是负逻辑的情况
- 先处理掉简单的情况
- 先处理有趣的或者是可疑的情况
?:条件表达式
- 默认情况下用if/else更清晰,条件表达式只有在最简单的情况下使用。
var a.b = new c(a.d ? a.e(1) : a.f(1));
if (a.d) {
var a.b = new c(a.e(1));
} else {
var a.b = new c(a.f(0));
}
避免 do/while 循环(逻辑颠倒不易读)
从函数中提前返回(处理异常情况、减少嵌套)
适当增加小括号(逻辑更清晰)
// 容易被 && 和 || 的优先级迷惑
(a + b > c && a - b < c || a > b > c)
// 优化顺序后,可读性更强
((a + b > c) && ((a - b < c) || (a > b > c))
2. 拆分超长的表达式
超长的表达式常常是不容易理解的,我们需要把一些变量拆分出来,下列类型的变量更适合单独拆分:
用作解释的变量
拆分表达式最简单的方法就是引入一个额外的变量,让它来表示一个小一点的子表达式。
// 👎 超长表达式难以理解
if (ListeningStateChangedEvent.split(':')[0].strip() === 'root') { }
// 👍 按照含义拆分username
username = line.split(':')[0].strip();
if (username === "root") { }
总结变量
总结变量的目的只是用一个短很多的名字来代替一大块代码,这个名字会更容易管理和思考。
// 👎
if (request.user.id === document.owner_id) {
// user can edit this document...
}
// 👍
let user_owns_document = (request.user.id === document.owner_id);
if (user_owns_document) {
// user can edit this document...
}
使用德摩根定理
德摩根定理的原则是:「分别取反,转换与或」。有时你可以使用这个法则来让布尔表达式更具可读性。
not(a or b or c) 等价于 (not a) and (not b) and (not c)
not(a and b and c) 等价于 (not a) or (not b) or (not c)
// 使用德摩根定理转换以前
if (!(file_exists && !is_protected)) {}
// 使用德摩根定理转换以后
if (!file_exists || is_protected) { }
3. 变量与可读性
减少变量
- 没有价值的临时变量:没有拆分任何复杂的表达式、没有做更多的澄清、只用过一次
- 减少中间结果:通过让代码提前返回来处理中间用来保存临时结果的变量
缩小变量的作用域
- 大文件拆分成小文件,大函数拆分成小函数,并且相互独立
- 把只有一个函数会用到的全局变量变成私有变量
- 对所有的引用使用 const ,使用 let 代替 var
三、重新组织代码
1. 抽取不相关的子问题
- 抽取纯工具代码
- 抽取多处复用的小模块
- 创建大量通用代码:例如util中的代码
- 项目专有的功能:把业务中特殊的代码单独处理
- 简化已有接口:例如
document.cookie提供了字符串,可以通过函数简化读取
这样做的好处:
- 提高代码的可读性:
将函数的调用与原来复杂的实现进行替换,让阅读代码的人很快能了解到该子逻辑的目的,让他们把注意力放在更高层的主逻辑上,而不会被子逻辑的实现(往往是复杂无味的)所影响。
- 便于修改和调试:
因为一个项目中可能会多次调用该子逻辑(计算距离,计算汇率,保留小数点),当业务需求发生改变的时候只需要改变这一处就可以了,而且调试起来也非常容易。
- 便于测试:
同理,也是因为可以被多次调用,在进行测试的时候就比较有针对性。
2. 一次只做一件事
-
列出代码所做的所有“任务”: 任务可以很小,比如检查数据格式。
-
尽量把这件任务拆分到不同的函数中,或者至少是代码中不同的段落中。
这样做的好处:
-
我们可以清晰地看到这个功能是如何一步一步完成的,而且拆分出来的小的函数或许也可以用在其他的地方。
-
如果你遇到了比较难读懂的代码,可以尝试将它所做的所有任务列出来。可能马上你就会发现这其中有些任务可以转化成单独的函数或者类。而其他的部分可以简单的成为函数中的一个逻辑段落。
3. 把想法变成代码
“如果你不能把一件事解释给你祖母听的话说明你还没有真正理解它。”
在设计一个解决方案之前,如果你能够用自然语言把问题说清楚会对整个设计非常有帮助。因为如果直接从大脑中的想法转化为代码,可能会露掉一些东西。但是如果你可以将整个问题和想法滴水不漏地说出来,就可能会发现一些之前没有想到的问题。这样可以不断完善你的思路和设计。
可以尝试以下方法:
- 想对着一个同事一样用自然语言描述代码要做什么
- 注意描述中所使用的关键词和短语
- 写出与描述所匹配的代码
4. 少写代码
-
质疑和拆分你的需求
不是所有的程序都需要运行得快,100%准确,并且能处理所有的输入。如果你真的仔细检查你的需求,有时你可以把它削减成一个简单的问题,只需要较少的代码。
-
保持小代码库
创建越多越好的“工具”代码来减少重复代码
让你的项目保持分开的子项目状态
-
熟悉你周边的库
每隔一段时间,花15分钟来阅读标准库中的所有函数/模块/类型的名字。
参考内容:
《编写可读代码的艺术》
《编写高质量代码:改善JavaScript程序的188个建议》
《Airbnb JavaScript 代码规范(ES6)》