文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

为一段代码取个好名字很重要。如果你的代码至少会被阅读一次,那么名字在你使用它进行工作时就显得很重要。

变量名称,函数名称,类的名称,接口里的名称,这一切都为了让阅读者知道一段代码要做的事情。在工作中进行代码审查时,我对团队成员在良好命名方面相当挑剔—很抱歉,伙计们!—但是我相信这确实可能会成就或毁坏我们代码的质量。

即使会有其他很多有价值的办法可以得知一段代码是做什么的,例如文档,但是良好的命名却是一个传递代码信息的极其有效的渠道,至少有两点原因:

第 1 段(可获 1.74 积分)
  • 非常棒的命名能够立即就告诉你你的代码里发生了什么,而对于需要在文档里查看然后通过按照它来看代码的情况来说,就没那么快速了。
  • 命名可以很快得到提高。你可以在代码里做快速的修复即可更新一些名字,手动或者借助于工具(比如,非常流行的clang-tidy),而且如果你的代码构建成功,那么你就可以几乎确定他通过了测试。

因为它是如此的重要,我将给大家提供一个如何选择良好命名的指南。这些指南中的某些部分我是借自Steve McConnell的参考书Code Complete(代码大全) (这是由John在他的文章 “如何开始软件开发”里建议必读的一本书。)

第 2 段(可获 1.56 积分)

本文中所提及的一些技巧是我和同事在工作中进行讨论、建议以及代码审查的过程中所得到的。而有很大一部分则是我经验累月通过自己编码试验所得出来的。

我们先来解释下如何避免糟糕的命名,然后集中讲述如何选择良好的命名。

不要使用任何不合法的东西

我们首先要明确一点:在特定的语言中有些命名是严禁使用的。

除了一些标准的预留关键字(如int,使用它们会导致编译失败)之外,每一种语言都有其关于合法命名的特定规则。比如在C++,在名字中使用下划线(_)的组合虽然能编译通过,但却是不合法的,因为它们是预留给编译器或标准库实现者使用的。使用它们会跟编译器或者标准库所声明的对象或例程产生冲突,从而导致细微错误或者意想不到的行为产生。

第 3 段(可获 1.91 积分)

以下是C++语言中为编译器和标准库实现者所预留的命名:

  • 任何包含连续下划线的名字(__)
  • 任何以一个下划线开始并紧跟一个大写字母的名字 (_isOk, isOk_too, _IsNotOk)
  • 在全局命名空间里以一个下划线开始的名字

不要考虑使用这样的命名,因为它们会给你带来麻烦。

不要浪费信息

就这一点来说,你的代码完全清楚自己在做些什么。事实上,它会尽可能忠实的执行它的内容,它最明白不过了!

第 4 段(可获 1.34 积分)

取个好名字实际上是关于如何尽可能的使代码保存足够多信息的问题。换言之,就是不能因为混淆代码而导致本来可以表达的信息被浪费掉。有趣的是,隐藏信息通常是被提倡的,通过封装来实现。但是在这个上下文里,我们是要披露信息

因此,要减少缩写的使用。 缩写和缩略词写起来很方便,但是读起来就没那么容易了。俗话说的好,代码是一次写成的,但是却要被读很多遍。

然而,你也没必要为了使代码更加清晰而把所有的缩略词都拼写完整,而且一些未经简略的重复代码还有可能会影响代码的可读性。

第 5 段(可获 1.41 积分)

比如,在你的代码里使用“VAT”来替代valueAddedTax,因为大家都知道VAT是什么东西。

那你该如何决定是否在代码里使用缩略词呢?经验法则:如果你的应用的终端用户能够理解一个缩写或缩略词的话,那么你使用它就是没问题的,因为在你的领域里的所有人都知道它是什么意思。

此外,不要试图为了得到最小数目的字符而去优化。在论坛里,你也许会看到有些人在讨论说他们的方法都多牛B,因为需要打的字比较少。但是这却更像是带来了麻烦:你少按几个按键,却让别人花了几分钟看代码来搞明白它是做什么的?

第 6 段(可获 1.65 积分)

你肯定不想浪费时间来弄明白一个函数或方法的名字是什么意思,特别是当你可以给它们尽可能长的命名时。 来自南安普敦大学的研究 (Rees 1982) 表明函数和方法的名字在合理的情况下可以最长至35个字符,这听起来有点多,但是一个长的名字可以帮助你一眼就能看明白一段代码的含义和它要做的事情。

然而,一个函数的名字也有可能因为以下糟糕的原因变得臃肿不堪:

  • 如果一个函数的名字过长是因为该函数所要完成的事情太多,那么就不能通过命名来解决这个问题了,而是可以将其分解为多个逻辑部分。
  • 有的时候,如果在函数名里包含已经由其参数类型所表达的多余信息的时候就会变得冗余过长。例如:
第 7 段(可获 1.85 积分)
void saveEmployee(Employee const& employee);

可以参考托管在GitHubrawsp_naming1.cpp文件,

可以重命名为:

void save(Employee const& employee);

可以参考托管在GitHubrawsp_naming2.cpp 文件,

这样在客户端进行调用时也会显得很自然:

save(manager);

可以参考托管在GitHubrawsp_naming3.cpp 文件,

而不是:

saveEmployee(manager);

可以参考托管在GitHubrawsp_naming4.cpp文件,

 

  • 否定是另外一个在命名中不被提倡使用的例子,因为它会迫使读者进行思考将其转换为其相反的含义从而才可以读明白代码。如下例子:
第 8 段(可获 1.26 积分)
if (isNotValid(id))
{

查阅托管在GitHub的文件 rawsp_naming5.cpp 

可以通过使用肯定的名字来改善:

if (!isValid(id))
{

查阅托管在GitHub的文件 rawsp_naming6.cpp

既然我们已经排除了一些坏的命名实践,那就让我们来看看如何挑选好的名字。

选择与抽象层次一致的名称

正如我在之前的文章中提到的, 慎重对待抽象层次是很多最佳实践的基石。这些最佳实践中,好的命名即是其中之一。

一个好的名字会跟周围代码的抽象层次保持一致。在关于抽象层次的文章中曾讲到,一个好的名字能够表达代码在做的事情,而非它如何去做这件事情。

第 9 段(可获 1.55 积分)

为了说明这一点,让我们来看一个一个函数,计算一个公司里所有员工的薪水。该函数返回一个集合,将键(employees)与值(salaries)关联起来。

一个糟糕的函数名,会着重于描述该函数如何实现,就如:

std::vector< pair<EmployeeId, double> > computeSalariesPairVector();

查看GitHub上托管的文件rawsp_naming7.cpp

这样的函数名的问题在于它表达了其是以成对向量的形式来计算结果,而没有展示其如何实现的,即计算员工的薪水。快速修复的办法是可以将名字更改为:

第 10 段(可获 1.45 积分)
std::vector< pair<EmployeeId, double> > computeEmployeeSalaries();

查看托管在GitHub上的文件rawsp_naming8.cpp

这样对于调用方来说可以减少一些实现的细节,使代码的读者能够理解代码本来要做的事情。

理解抽象层次有助于对变量和对象进行命名。通常在代码中,变量和对象表示比它们的类型更抽象的东西。

例如,一个整型(int)通常表示的含义不止是一个int:它可以标识一个人的年龄或者一个集合中的元素的数量。或者,一个Employee类型的对象可以标识一个成员的管理者。或者,一个std::vector<double> 可以表示纽约上个月每天的平均气温。(当然,这种情况并不适用于一些相当底层的代码中,比如对两个整型进行相加,或者在某些使用强类型的场合中。)

第 11 段(可获 1.86 积分)

在这种情况下,你最好以变量所表达的含义来命名该变量而不是以它的类型来命名。比如说你将一个整型变量命名为“age”,而不是“i”。还有将上面的Employee命名为‘manager’,而不是‘employee’。或者将向量命名为‘temperatures’,而不是‘doubles’。

这看起来相当明显,然而在某些情形下我们一般会忽略这条准则。让我们拿iterators 和templated types 来举个例子。

假设我们从一个金融产品获得了一系列的现金流。一些现金流是正的;一些是负的。我们想要检索出第一批流向我们的现金流,这样我们就可以专注于这第一批正的现金流。以下是我们第一次尝试写的一段代码:

第 12 段(可获 1.6 积分)
std::vector<CashFlow> flows = ...



auto it = std::find_if(flows.begin(), flows.end(), isPositive);

std::cout << "Made " it->getValue() << "$, at last!\n";

查看托管在GitHub上的文件rawsp_naming9.cpp

这段代码使用了‘it’的命名,显示了该变量是如何实现的(使用了一个迭代器),而不是该变量的含义。对比如下代码,你觉得如何?

std::vector<CashFlow> flows = ...



auto firstPositiveFlow = std::find_if(flows.begin(), flows.end(), isPositive);

std::cout << "Made " << firstPositiveFlow->getValue() << "$, at last!\n;
第 13 段(可获 1.06 积分)

查看托管在GitHub上的文件rawsp_naming10.cpp

哪段代码可以让你更容易理解?你能想象你不需要阅读两行的代码与10行或50行的代码有什么区别吗?请注意,这与我们之前关于不要浪费代码本身可以表达的信息的部分有所关联。

同样的逻辑也适用于模板参数。特别是当你学习使用模版的时候,我们可以看到大量的从学术资源里看到的样例,我们会倾向于编写如下模版类和函数:

第 14 段(可获 1.26 积分)
template <typename T>

查看托管在GitHub上的文件rawsp_naming11.cpp 

…然而你可能会知道更多关于T的信息,它只是一个类型。

在一些非常通用的代码中,如果你不知道任何关于该类型的信息,那么使用T作为类型名字是没问题的,就如在std::is_const里:

template<typename T>

struct is_const;

查看托管在GitHub上的文章rawsp_naming12.cpp

但是关于T所表达的任何信息都应该在你的代码里能够找到。让我们看一个简单的例子,是一个用于解析一个序列化输入的函数:

template <typename T>

T parse(SerializedInput& input)
{
T result;
// ... perform the parsing ...
return result;
}

 

第 15 段(可获 1.38 积分)

 查看托管在GitHub上的文件rawsp_naming13.cpp

以下代码展示了如何更明确的表达T所代表的含义:

template <typename ParsedType>

ParsedType parse(SerializedInput& input)

{

ParsedType result;

// ... perform the parsing ...

return result;

}

查看托管在GitHub上的文件rawsp_naming14.cpp

对比这两段代码看看,你认为哪一段更加容易理解?我肯定会选择第二段代码的,因为它告诉了我们该类型是需要被解析的,而第一个只能看出T是一个类型而已。

你也许会认为对类型进行命名会带来很大的差异,或者你不这么认为。但是可以肯定的是,第二段代码包含了更多的文档在里面,并且是免费提供的。 在你的代码里将一个糟糕的名字替换为一个好的名字,特别是一个本地使用的名字,所能花费的时间几乎可以忽略不计,而由一个更加清晰的命名所导致的性能成本损耗也完全等于零。

大体来讲,对于好的命名来说,这种成本确实是不存在的:这是一顿免费的午餐,因此,让我们好好利用它吧。

第 16 段(可获 2.38 积分)

文章评论