编写可读代码(1)

昨天看到一本书《The Art of Readable Code》 by Dustin Boswell,and Trevor Foucher 视为经典,读了两章之后越发觉得This is a fine book.在这里总结一下自己的所学。


我希望你在阅读之前先回忆一下自己的编程习惯,自己的代码可读性如何?、是否易于理解?

你是否有过这样的经历(在阅读自己以前所写的代码时,发现已经忘记了这些代码的含义)?你是否有自己的编程习惯?

如果你是具有良好编程习惯,并且具有很高可读性的程序猿,你可以选择忽略这篇文章,如果你正在为自己的编程习惯而发愁,迫切的想要提高自己代码的可读性,使自己的代码变成“好”代码,我们可以一起学习,一起成为这本书的受益者。

代码应当易于理解

关键思想

  1. 代码应当易于理解
  2. 代码的写法应当使别人理解他所需的时间最小化

从表面层次改进我们的代码

首先我们先从表面层次改进我们的代码:选择好的名字、写好的注释以及把代码整洁地写成更好的格式。这些改变很容易应用。你可以在“原位”做这些改变而不必重构代码或者改变程序的运行方式。

这一点很重要,因为会影响到你代码库中的每一行代码。尽管每个改变可能看上去都很小,聚集在一起造成代码库巨大的改进。如果你的代码有很棒的名字,写得很好的注释,并且整洁的使用了空白符,你的代码就会变的易读的多。

把信息装到名字里

无论是命名变量、函数还是类,都可以使用很多相同的原则。名字是一条很好的注释。选择一个好的名字可以让他承载很多信息。

有6种方法可以帮助我们把信息装进名字里

  • 选择专业的词
  • 避免泛泛的名字(或者说要知道什么时候使用它)
  • 用具体的名字代替抽象的名字
  • 使用前缀或者后缀来给名字附带更多信息
  • 决定名字的长度
  • 利用名字的格式来表达含义

选择专业的词

例如 “get”这个词就很不专业

1
def GetPage(url): ...

“get”这个词并没有表达出很多信息。这个方法是从本地缓存得到一个页面,还是从数据库中,或者是从互联网中?如果是从互联网中,FetchPage()或者DownloadPage()会更专业
再例如:

1
2
3
4
class BinaryTree{
int Size();
...
}

只通过size这个名字我们无法得知size()方法返回的什么,树的高度?节点数?还是树在内存中所占的空间?
我们可以通过选择跟专业的词Height()、NumNodes()或者MemoryBytes()来代替size
我们还可以利用英语的丰富性来帮助我们 找到更好更专业的词
QQ截图20180411213041.png

避免使用泛泛的词(例如:tmp retval)

与其使用一个空洞的词,不如挑选一个更有意义的词
例如:

1
2
3
4
5
6
var euclidean_norm = function(v) {
var retval = 0.0;
for (var i = 0; i < v.length; i ++)
retval += v[i]*v[i];
return Math.sqrt(retval);
}

retval除了“我是一个返回值”的意思外 并没有包含更多信息
好的名字往往可以描述变量的目的或者它所承载的值。在上面的例子中,这个变量正在累加v的平方。因此用sum_squares会更贴切些,而且可能会帮助我们找到缺陷。
假如我们在循环里意外写成了

1
retval += v[i];

如果名字换成sum_quares这个缺陷就会很明显l:

1
sum_squares += v[i];  // 我们要累加的是square

如果一个变量至应用与短期存在且临时性为其主要存在因素的变量是tmp这种泛泛的词也不失为一种“好”选择

再例如:
像i,j,iter等名字,常用作索引或者循环迭代器。尽管这些名字很空泛,但大家都知道他们的意思是“我是一个索引,迭代器”(事实上,如果你用这些名字来表示其它含义,那会很混乱。所以不建议这么做)
但是有时会有比i,j,k更加贴切的迭代器命名。例如下面这个循环要找到那个user属于那个club:

1
2
3
4
5
for (int i = 0; i < cluds.size(); i ++)
for (int j = 0; j < clud[i].members.size(); j ++)
for (int k = 0; k < users.size(); k ++)
if (clubs[i].members[k] == users[j])
cout <<"user["<<j<<"] is in club["<< i <<"]" << endl;

在if条件语句中,members[]和users[]用了错误的索引。这样的缺陷是很难发现的,因为这一行行的代码单独来看似乎是没什么问题的。

1
if (clubs[i].members[k] == users[j])

在这种情况下,使用更精确的名字可能会有帮助。例如将(i, j, k)这些索引节点换成(club_i, members_i, user_i)或者更简化一点(ci, mi, ui)这样会帮我们把代码中的缺陷变得更加明显。

1
if (clubs[ci].members[mi] == users[mi])  // error

如果使用的正确索引的第一个字母应该与数据的第一个字母匹配:

1
if (clubs[ci].members[mi] == users[ui])  // OK

用更具体的名字来替代抽象的名字

用Google代码库的一个例子。在C++中,如果你不为类定义拷贝构造函数或者复制操作符,那就会有一个默认的。尽管这样很方便,这些方法很容易导致内存泄漏以及其它灾难,因为他们在你可能想不到的幕后工作。
所以,Google有个便利的方法来禁止这些方法。就是下面这个宏

1
2
3
4
5
class ClassName {
private:
DISALLOW_EVIL_CONSTRUCTORS(ClassName);
public: ...
};

这个宏定义成:

1
2
3
define  DISALLOW_EVIL_CONSTRUCTORS(ClassName);
ClassName(const ClassName&);
void operator=(const ClassName&);

通过这个宏放在类的私有部分中,这两个方法变成私有的,所以不能使用他们。
但是这个宏禁止了什么是不清楚的,所以最终换成了

1
define  DISALLOW_COPY_AND_ASSIGN(ClassName);

添加前缀或后缀为名字附带更多信息

带单位的值

如果变量是一个度量的话(时间,长度,字节数),那么最好把名字带上它的单位
QQ截图20180411213138.png
我们也可以在名字后面添加上一些额外信息
匈牙利表示法也是我们可以学习的好方法。

名字应该有多长

当取一个好名字时,有一个隐含的约束条件是名字不能太长。
当然也不能太短。

在小的作用域里可以使用短的名字

短期度假时,我们带的行李往往比长假少。同样的,“作用域小”的标识符(对于多少行其它代码可见)也不用带上太多信息

1
2
3
4
5
if (debug) {
map<String, int> m;
LookUpNamesNumbers(&m);
print(m);
}

尽管m这个名字并没有包含很多信息,但这不是问题,因为我们已经能够理解这段代码的所有信息
然而,假设m是一个全局变量中的类成员,如果你看到这样的代码片段

1
2
LookUpNamesNumbers(&m);
print(m);

这段代码就没那么好读了

丢掉没有的词

有事名字中的某些单词去掉也不会损失任何信息。例如ConvertToString()就不如ToString()这个短名字

利用名字的格式勒传递含义

对于下划线,连字符和大小写的使用方式也可以把更多信息装进名字里。
对于不同的实体使用不同的格式就像语法高亮显示的形式一样,能帮助我们更好的阅读代码。
以一条下划线来区分成员变量和普通变量。

一旦你使用了这些规范,就一定要在整个程序或者项目中保持一直

未完待续…