我终于开始写技术类文章。
首先感谢 「一口程序锅」、「labuladong」两位公号主对我直接和间接的帮助。
为啥写这个炸鸡
我一开始其实是想写设计模式,写了一定的积累。虽然我也想写比较高端的算法,数据结构,甚至 AI 的东西。但是很无奈,现在能力不足无法下笔。
但是在写代码的过程中,我逐渐发现一个问题,不仅是在学习还是工作上。
包括我在内,许多人的代码可读性其实一塌糊涂。先不从代码组织,设计模式这些较大的方面来说。光是一个变量,一个函数的命名,注释的规范都没有提供帮助理解 的作用,让人看的一头雾水。
起码我看我自己的代码就是这个感觉,几个月后,就不认识了。
代码的编写规范,是很少人去注意的,这段时间,我的主程让我看一本书 ——《编写可读代码的艺术》,正好直击痛点,于是我打算写一写关于这个方面的东西。
希望通过这个系列的炸鸡,能让我和各位在代码可读性上,有所进步。
前提准备
在阅读本篇前,先得了解一些原则。
基本可读性定理
代码应该写得让人在最短时间内理解。理解的含义包括,完全理解,并且能定位问题,修改,还有能看出这段代码和其他代码的联系。
对自己的要求
多问问自己,这段代码真的容易理解吗?想象另外一个人在看自己的代码,或者有一个与你志同道合的人加入了你的项目组,哪怕只有一个人。他接手阅读这些代码的时候,是紧锁眉头,还是眉头舒展。
好了,我们开始吧。
命名需要增加更多的信息
我们先从最容易更改的方面来说,就是命名。一般来说,很多人写程序的时候对于命名是比较苦恼的,写程序一半的时间在思考命名。或者有时候,图方便,将命名写的很随意。
由于这个代码是自己写的,一两天内,浏览下来仍然清楚这些命名下的变量,函数等,它们的作用于意义。
但是再过些时日再看,你自己都会摸不着头脑了,这样代码的可读性是很差的。
所以,命名应该表达更多的信息。
用意思更明确的词语
为了命名能表达更多的信息,命名的用词需要更加具体,我们应该用意思更明确的词语去做命名。
我们看一下如下代码,这是一个描述文本文件的类。
1class Text
2{
3public:
4 Text();
5 ~Text();
6 ...
7 int size();
8};
size 的使用可谓是一个重灾区,乍一看没什么问题。但是这个 size 有一些问题:
-
size 多为属性命名,以至于这是函数调用还是公共属性,会使阅读者感到困惑。
-
size 意义不明朗。这是代表文件的大小,还是文件内容的长度,或者说,单词数?
-
size 如果是函数,返回的是当前文件大小,还是文件限定大小,是文件内容的当前大小,还是……。
看到了吧,一个意味不明的命名可以带来多少阅读理解上的麻烦。
如果我们使用 getCurrentBytes()
,就能大概率理解为:
获得此文件的大小的函数。
如果我们使用 countWordNum()
,就能大概率理解为:
计算获得文件内容的单词数量的函数。
所以,命名的时候选词要选择意义明确,能准确表达作用和目的的词。
丰富的词语
为了能表达更明确的意思,那么我们的英语词汇量其实是要足够的。
这就需要我们多多使用翻译软件,多多积累词汇量了。
在书中提供了一个示例,我将其列出。
一般用词 | 更加丰富多义的词语 |
---|---|
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 |
避免不明确、宽泛的用词
从上文我们知道,命名要更加具体,选用表达更加准确的词语。
所以,我们不仅要会选择,还要回规避。
规避一些意义不明确的词语。
注意,这个意义不明确,是针对需要表达许多信息这种情况来说的。
用两个重灾区举例
1. 返回值。
我们来看这段代码。
1local ret = Calculator:calculate(1, 2)
这个 ret
,我们都能意识到他是一个返回值。但是这样表达的信息实在太少,从而导致意义不明确。
如果我用的是 iret
,这就多了一个数据类型的信息,告诉阅读者:
这个是一个整型的返回值。
如果使用 multiplyRet
,这就将 calculate 的逻辑大致概括,告诉阅读者:
使用乘法运算得到结果。
2. tmp
一样的,来看一段代码。
1tmp = ""
2tmp += 'name:'
3tmp += '\t{}\n'.format('dogge')
4tmp += 'age:'
5tmp += '\t{}\n'.format(122)
6tmp += 'email:'
7tmp += '\t{}\n'.format('545749402@qq.com')
8tmp += 'address:'
9tmp += '\t{}\n'.format('地球')
很明显,tmp 只表达了自己字符串拼接的暂存变量。但是从结果来看,它更适合取名为userInfo
。阅读者看到这个命名,不仅了解了更多的信息,还不用阅读这么多行才能分析出这个变量的意义。
但是如同之前说的,如果只需要表达单一的意思,例如 tmp 只用于暂存变量。那么,tmp 这个命名是很合适的。
循环迭代的不明确
循环迭代的变量,最经典的命名就是i, j,k
。
我们来看一下如下代码,代码功能是去遍历每个年级的每个班级的每个同学的名字。
1for (int i = 0; i < grades.size(); i++)
2 for (int j = 0; j < grades[i].class.size(); j++)
3 for (int k = 0; k < classMember.size(); k++)
4 cout << classMember[k].name << endl;
可以看到,循环逻辑如果复杂,i, j, k
在不同的嵌套中互相使用对方,错综复杂。编写这段代码的人都要小心 i,j,k
是否使用出错,阅读者的阅读难度也加大。
所以,循环变量不要宽泛不明确,也要加入更多的信息。
将 i, j, k
修改为 grade_i, class_j, member_k
。修改代码如下:
1for (int grade_i = 0; grade_i < grades.size(); grade_i++)
2 for (int class_j = 0; class_j < grades[grade_i].class.size(); class_j++)
3 for (int member_k = 0; member_k < classMember.size(); member_k++)
4 cout << classMember[member_k].name << endl;
这样方便阅读者的理解,不用花精力去记住 i, j, k
对应的数据,同时定位变量使用的错误 grades[class_j]
也更方便。
准确 > 宽泛
这样的情况通常发生在函数命名。有时候我们的函数命名偏向做概括,使得命名过于宽泛。
如下代码就是一个例子,编写者想的是,服务器是否准备就绪,只需要检查一下端口是否被占用即可。
1bool serverIsReady()
2{
3 // 检查端口是否被占用
4 ...
5}
调用这个函数时返回 true,但由于 IP 问题,服务器启动失败。阅读者看着这个命名,就会疑惑,不是服务器准备就绪了吗?
1if (serverIsReady())
2{
3 launchServer(); // 失败
4}
这就是过于命名宽泛的结果,我们把函数名修改为 canListenPort
,就准确地描述了函数功能。阅读者就知道,这只是检查端口占用,IP 等其他环境没有做检查。也方便了阅读者做一定的封装和修改。
命名可以加入什么信息
上头一直在讲,命名要加入更多信息,怎么加入更多的信息。
所以现在讲讲可以加入什么信息。
单位
说实话,看到这个的时候,我有一种恍然大悟的感觉。先前写一些与时间相关的逻辑,都会疑惑。
1local starttime = os.clock()
2
3...
4
5local elapse = os.clock()
6print(elapse) -- 1
这算出来,到底是 1 ms, 1 us, 1 s ?当然,你可以说我对 lua 的 os.clock
不熟悉。那么如果是编写者自己封装的函数呢。
1local starttime_ms = myClock()
2
3...
4
5local elapse_ms = myClock() - starttime_ms
所以类似的,测量性质的变量,最好都要加上单位。
以下是书中的实例,我截取出来。
没有使用单位 | 使用单位 |
---|---|
Start(int delay ) | delay → delay_secs |
CreateCache(int size ) | size → size_mb |
ThrottleDownload(float limit) | limit → max_kbps |
Rotate(float angle) | angle → degrees_cw |
重要的属性
什么是重要的属性?就是一定要阅读者知道的重要信息。例如,你的逻辑中,需要一个字符串。但是这个字符串内容是 十六进制 字符串。
如果光光一个 str
,是不能表达这个信息的。
1local str
所以我们可以修改成:
1local hex_str
再考虑一个场景,后台对接收到的密码进行处理。但是要求入库编码要求是 utf8,并且是明文密码。一个 password
,明显不能表达更多信息。
那我们试试这个:
1local plaintext_utf8_password
总结
本次炸鸡主要是针对命名信息过于匮乏的问题,从两个方面来阐述了供参考的解决方案。
-
命名如何加入更多的信息。
-
命名加入什么信息。
从 1 来说,命名需要表达具体。所以 1 展开分为两个部分。
-
由于命名需要表达具体,那么用词需要准确和具体地描述变量/函数作用和逻辑。
-
既然知道了要具体,那么就要规避宽泛的用词。
从 2 展开,主要讲了
-
测量类型变量,需要加入单位
-
变量的重要属性,需要将这个属性加入命名之中。
参考资料
《The Art of Readable Code》
不甘于「本该如此」
可以加入我们的星球,一起交流学习。