阅读 128

可读代码编写炸鸡一 - 命名

我终于开始写技术类文章。

首先感谢 「一口程序锅」、「labuladong」两位公号主对我直接和间接的帮助。

为啥写这个炸鸡

我一开始其实是想写设计模式,写了一定的积累。虽然我也想写比较高端的算法,数据结构,甚至 AI 的东西。但是很无奈,现在能力不足无法下笔。

但是在写代码的过程中,我逐渐发现一个问题,不仅是在学习还是工作上。

包括我在内,许多人的代码可读性其实一塌糊涂。先不从代码组织,设计模式这些较大的方面来说。光是一个变量,一个函数的命名,注释的规范都没有提供帮助理解 的作用,让人看的一头雾水。

起码我看我自己的代码就是这个感觉,几个月后,就不认识了。

代码的编写规范,是很少人去注意的,这段时间,我的主程让我看一本书 ——《编写可读代码的艺术》,正好直击痛点,于是我打算写一写关于这个方面的东西。

希望通过这个系列的炸鸡,能让我和各位在代码可读性上,有所进步。

前提准备

在阅读本篇前,先得了解一些原则。

基本可读性定理

代码应该写得让人在最短时间内理解。理解的含义包括,完全理解,并且能定位问题,修改,还有能看出这段代码和其他代码的联系。

对自己的要求

多问问自己,这段代码真的容易理解吗?想象另外一个人在看自己的代码,或者有一个与你志同道合的人加入了你的项目组,哪怕只有一个人。他接手阅读这些代码的时候,是紧锁眉头,还是眉头舒展。


好了,我们开始吧。

命名需要增加更多的信息

我们先从最容易更改的方面来说,就是命名。一般来说,很多人写程序的时候对于命名是比较苦恼的,写程序一半的时间在思考命名。或者有时候,图方便,将命名写的很随意。

由于这个代码是自己写的,一两天内,浏览下来仍然清楚这些命名下的变量,函数等,它们的作用于意义。

但是再过些时日再看,你自己都会摸不着头脑了,这样代码的可读性是很差的。

所以,命名应该表达更多的信息

用意思更明确的词语

为了命名能表达更多的信息,命名的用词需要更加具体,我们应该用意思更明确的词语去做命名。

我们看一下如下代码,这是一个描述文本文件的类。

class Text
{
public:
    Text();
    ~Text();
    ...
    int size();
};
复制代码

size 的使用可谓是一个重灾区,乍一看没什么问题。但是这个 size 有一些问题:

  1. size 多为属性命名,以至于这是函数调用还是公共属性,会使阅读者感到困惑。
  2. size 意义不明朗。这是代表文件的大小,还是文件内容的长度,或者说,单词数?
  3. 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. 返回值。

我们来看这段代码。

local ret = Calculator:calculate(1, 2)
复制代码

这个 ret,我们都能意识到他是一个返回值。但是这样表达的信息实在太少,从而导致意义不明确。

如果我用的是 iret,这就多了一个数据类型的信息,告诉阅读者:

这个是一个整型的返回值。

如果使用 multiplyRet,这就将 calculate 的逻辑大致概括,告诉阅读者:

使用乘法运算得到结果。

2. tmp

一样的,来看一段代码。

tmp = ""
tmp += 'name:'
tmp += '\t{}\n'.format('dogge')
tmp += 'age:'
tmp += '\t{}\n'.format(122)
tmp += 'email:'
tmp += '\t{}\n'.format('545749402@qq.com')
tmp += 'address:'
tmp += '\t{}\n'.format('地球')
复制代码

很明显,tmp 只表达了自己字符串拼接的暂存变量。但是从结果来看,它更适合取名为userInfo。阅读者看到这个命名,不仅了解了更多的信息,还不用阅读这么多行才能分析出这个变量的意义。

但是如同之前说的,如果只需要表达单一的意思,例如 tmp 只用于暂存变量。那么,tmp 这个命名是很合适的。

循环迭代的不明确

循环迭代的变量,最经典的命名就是i, j,k

我们来看一下如下代码,代码功能是去遍历每个年级的每个班级的每个同学的名字。

for (int i = 0; i < grades.size(); i++)
    for (int j = 0; j < grades[i].class.size(); j++)
        for (int k = 0; k < classMember.size(); k++)
            cout << classMember[k].name << endl;
复制代码

可以看到,循环逻辑如果复杂,i, j, k 在不同的嵌套中互相使用对方,错综复杂。编写这段代码的人都要小心 i,j,k 是否使用出错,阅读者的阅读难度也加大。

所以,循环变量不要宽泛不明确,也要加入更多的信息

i, j, k 修改为 grade_i, class_j, member_k。修改代码如下:

for (int grade_i = 0; grade_i < grades.size(); grade_i++)
    for (int class_j = 0; class_j < grades[grade_i].class.size(); class_j++)
        for (int member_k = 0; member_k < classMember.size(); member_k++)
            cout << classMember[member_k].name << endl;
复制代码

这样方便阅读者的理解,不用花精力去记住 i, j, k 对应的数据,同时定位变量使用的错误 grades[class_j] 也更方便。

准确 > 宽泛

这样的情况通常发生在函数命名。有时候我们的函数命名偏向做概括,使得命名过于宽泛。

如下代码就是一个例子,编写者想的是,服务器是否准备就绪,只需要检查一下端口是否被占用即可。

bool serverIsReady()
{
    // 检查端口是否被占用
    ...
}

复制代码

调用这个函数时返回 true,但由于 IP 问题,服务器启动失败。阅读者看着这个命名,就会疑惑,不是服务器准备就绪了吗?

if (serverIsReady())
{
    launchServer(); // 失败
}
复制代码

这就是过于命名宽泛的结果,我们把函数名修改为 canListenPort,就准确地描述了函数功能。阅读者就知道,这只是检查端口占用,IP 等其他环境没有做检查。也方便了阅读者做一定的封装和修改。

命名可以加入什么信息

上头一直在讲,命名要加入更多信息,怎么加入更多的信息

所以现在讲讲可以加入什么信息

单位

说实话,看到这个的时候,我有一种恍然大悟的感觉。先前写一些与时间相关的逻辑,都会疑惑。

local starttime = os.clock()

...

local elapse = os.clock() 
print(elapse) -- 1
复制代码

这算出来,到底是 1 ms, 1 us, 1 s ?当然,你可以说我对 lua 的 os.clock 不熟悉。那么如果是编写者自己封装的函数呢。

local starttime_ms = myClock()

...

local 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,是不能表达这个信息的。

local str
复制代码

所以我们可以修改成:

local hex_str
复制代码

再考虑一个场景,后台对接收到的密码进行处理。但是要求入库编码要求是 utf8,并且是明文密码。一个 password,明显不能表达更多信息。

那我们试试这个:

local plaintext_utf8_password
复制代码

总结

本次炸鸡主要是针对命名信息过于匮乏的问题,从两个方面来阐述了供参考的解决方案。

  1. 命名如何加入更多的信息。
  2. 命名加入什么信息。

从 1 来说,命名需要表达具体。所以 1 展开分为两个部分。

  1. 由于命名需要表达具体,那么用词需要准确和具体地描述变量/函数作用和逻辑。
  2. 既然知道了要具体,那么就要规避宽泛的用词。

从 2 展开,主要讲了

  1. 测量类型变量,需要加入单位
  2. 变量的重要属性,需要将这个属性加入命名之中。

参考资料

《The Art of Readable Code》

最后吟唱

本文使用 mdnice 排版