表面层次的改进

我们可读性游览的开始,是我们认为的那些“表面层次”的改进:挑选好的名字,编写好的注释,和代码格式整洁。这些类型的改变易于应用。你可以“就地”完成它们,而不需要重构你的代码或改变你的程序的运行。你可以增量的完成它们,而不需要大块的时间投入。

这些主题非常重要, 因为它们影响你的代码库中的每一行代码。 虽然每一个改变看起来很小,但聚集起来,它们会对一个代码库作出巨大的改进。如果你的代码有很好的名字,好的注释,和干净的使用空白,你的代码就会更容易阅读。

当然,表面层次变得易读(在本书的后续章节中,我们将讨论到)会带来很多好处。但在这部分的材料是如此广泛适用的,因此付出一点努力,首先讨论它是值得的。

2. 包装信息到名字

_images/2.png

无论你命名一个变量,函数,或类,有很多原则可以使用。我们喜欢把名字认为是一个微笑的注释。即使没有太多的空间(room),选择一个好的名字,可以传达大量的信息。

KEY IDEA

包装信息到你的名字

我们在程序里看见了大量的名字都是含糊的,例如 tmp 。即使单词看起来可能是合理的,例如 sizeget ,并没有包装许多信息。本章将告诉你如何选择名字。

本章安排了六个特定的主题:

  • 选择特定的词
  • 避免使用通用的名字(或知道什么时候使用它们)
  • 使用具体的名字代替抽象的名字
  • 决定名字有多长
  • 使用名字格式化来包装额外的信息

2.1. 选择特定的词

“包装信息到名字”的要素是选择一个特定的词,并避免“空的”词。

例如,”get”这个词不是非常特定,在这个例子中:

def GetPage(url):
    ...

“get”这个词确实没有说什么。这个方法是从本地缓存,从一个数据库,或从网络得到一页吗?如果它从网络,一个更特定的名字,应该是 FetchPage()DownloadPage()

这里是一个关于 BinaryTree 类的例子:

class BinaryTree {
    int Size();
    ...
};

你期望的 Size() 方法的返回值是什么?树的高度,节点的数目,还是树的足迹内存?

问题是 Size() 方法并没有传达太多的信息。一个更特定的名字应该是 Hight() ,`NumNodes()` 或 MemoryBytes()

另外一个例子,假如你有某种形式的 Thread 类:

class Thread {
    void Stop();
    ...
};

这个 Stop() 的名字是可以的,但这取决于它确切的干了什么,这应该有一个更特定的名字。例如,你可以叫它 Kill() ,如果它是一个重量级的不可以被撤销的操作。或你应该叫它 Pause() ,如果有一个方法来 Resume() 它。

2.1.1. 找到更“富有色彩”词

_images/2-1.png

不要害怕使用专业词典或向朋友询问一个更好名字的的建议。英语是一个丰富的语言,并有很多词从中选择。

这里有一些关于一个词,以及它的一些“富有色彩”的版本,可能会使用你的情形:

Word Alternatives
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

但是,不要得意忘形。在PHP中,有一个函数 explode() 一个字符窜。这是一个富有色彩的名字,它描绘了一个把一些东西打成碎片的好的图案,但是,它和 split() 有多大的区别呢?(这两个函数不不同的,但是,基于它们的名字很难猜测出它们的不同。)

KEY IDEA

是明确的和精确的比是可爱的更好。

2.2. 避免像 tmpretval 这样通用的名字

tmpretvalfoo 这样的名字,通常是为了逃避,意思是“我想不出一个名字” 选择一个名字描述实体的价值和目的 ,而不是使用像这样的一个空名字。

例如,这是一个 JavaScript 函数,使用了 retval

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

当你想不出一个更好的名字给你的返回值,使用 retval 是很有吸引力的。但 retval 除了表示“我是一个返回值”(这总是明显的)之外,并不能包含更多的信息

一个更好的名字应该可以描述变量或它所含的值的目的。在这种情况,变量是序列v的累加值。因此,一个更好的名字是 sum_squares 。这可以显示变量的目的,并且可能会抓到一个bug。

例如,假想如果循环的内部发生意外:

retval += v[i];

这一bug可以很明显,如果名字是 sun_squares

sum_squares += v[i]; // 我们在哪个序列求和?Bug!

ADVICE

retval这个名字并不能包装更多信息。代之,使用一个可以描述变量的值的名字

然而,在一些情况下,通用的名字确实可以承载含义。让我们来看看,什么时候使用它们是有意义的。

2.2.1. tmp

考虑一个典型的例子,交换两个变量:

if (right < left) {
    tmp = right;
    right = left;
    left = tmp;
}

在这种情况下,tmp 是一个很好的名字。这个变量唯一的目的是临时存储,只有很少几行的生命周期。 tmp 这个名字给读者传达了一个特定的意思——这一变量没有其它任何职责。它不会被传递到其它函数或被赋值或被多次使用。

但在下面的情况里, tmp 被使用只是处于懒惰:

String tmp = user.name();
tmp += " " + user.phone_number();
tmp += " " + user.email();
...
template.set("user_info", tmp);

即使这一变量有一个短暂的生命期,但作为临时存储并不是这一变量最重要的事情。相反,一个向 user_info 的名字,可能会更具描述性。

在下面的情况中, tmp 应该在名字里,但仅仅作为名字的一部分:

tmp_file = tempfile.NamedTemporaryFile()
...
SaveData(tmp_file, ...)

注意到,我们命名变量是 tmp_file ,而不仅仅是 tmp ,因为它是一个文件对象。想象一下,如果我们仅仅叫它 tmp

SaveData(tmp, ...)

只看代码的这一行,并不明确, tmp 是一个文件,文件名,或甚至是被写入的数据。

ADVICE

名字 `tmp` 应该只被用于短生命期和临时的情况,是这一变量最重要的事实。

2.2.2. 循环迭代器

向i,j,iter和it这样的名字,经常被用作索引和循环迭代器。即使这些名字是通用的, 它们被理解为“我是一个迭代器。”(事实上,你如果把这些名字用于 其它 目的,有可能会引起混乱——因此,不要这样做!)

但是,在有些时候,有比i,j和k更好的迭代器名字。例如,下面的循环找出哪一个用户属于哪一个俱乐部:

for (int i = 0; i < clubs.size(); i++)
  for (int j = 0; j < clubs[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 语句中, menbers[]users[] 被使用了错误的索引。像这样的bug难以定位,因为,隔开来看,这行代码似乎是好的:

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

在这种情况下,使用更明确的名字,会是有帮助的。替代(i,j,k)循环索引命名的另一个选择可能是(clud_i, member_i, users_i)或更紧凑的(ci,mi,ui)。这种方法有助于更加突出bug:

if (clubs[ci].members[ui] == users[mi]) # Bug!第一个字母不匹配。

当使用正确,索引的第一个字母因该与数组的地一个字母匹配:

if (clubs[ci].members[mi] == users[ui]) # 好的。第一个字母匹配。

2.2.3. 通用名字的结论

正如你所见到的,在一些情况下,通用名字是很有用的。

ADVICE

如果你打算使用 `tmp` , `it` ,或 `retval` 这样的通用名字,需要有一个好的原因。

大多数时候,纯粹出于懒惰,它们被过度的使用。这是可以可以理解的——当想不出更好的名字的时候,很容易使用像 like 这样的无意义的名字,然后继续。但如果你养成花费额外的几秒钟时间,提出一个好名字的习惯,你将发现你的“命名肌肉”能迅速建立。

2.3. 喜欢具体的而不是抽象的名字

_images/2-2.png

当命名一个变量,函数,或其它的元素时,要具体的而不要抽象的描述它。

例如,假设你有一个叫 SeverCanStart() 的内部方法,它测试服务是否在给定的 TCP/IP 端口上监听。可是, ServerCanStart() 这一名字有一些抽象。一个更具体的名字可能是 CanListenOnPort() 。这个名字直接描述了这个方法将要做的。

接下来的两个列子更深入地说明了这一概念。

2.3.1. 例:DISALLOW_EVIL_CONSTRUCTORS

这是一个从Google代码哭拿来的例子。在C++中,如果你没有给你的类定义一个拷贝构造函数或赋值操作符,将会有一个默认的。这些方法虽然简单,但可以很容易的导致内存泄漏和其它灾难,因为它们在“幕后”默默执行,你可能还没有意识到。

结果,Google有一个约定,不允许使用这样“邪恶”的构造函数使用下面的这个宏替:

class ClassName {
private:
    DISALLOW_EVIL_CONSTRUCTORS(ClassName);
public:
    ...
};

这个宏被定义为:

#define DISALLOW_EVIL_CONSTRUCTORS(ClassName) \
    ClassName(const ClassName&); \
    void operator=(const ClassName&);

通过把这个宏放在 private 中:类的一个区段,这两个方法成为了私有的,因此它们不能被使用,即使是不小心。

DISALLOW_EVIL_CONSTRUCTORS 这个名字不是很好。使用“邪恶”这个词,传达了一个值得商榷的问题上过于强硬的立场。更重要的是,它没有清楚的描述这个宏不允许什么。它不允许 operator()= 方法,但该方法并不是一个“构造函数”!这个名字使用了好几年,但最终被一个少了一些挑逗和更具体的替代了:

#define DISALLOW_COPY_AND_ASSIGN(ClassName) ...

2.3.2. 例:–run_locally

我们的一个程序有一个可选的命令行标志,叫做 –run_locally 。这个标志可使程序打印额外的调试信息,但会使程序运行的更慢。这个标志典型的用在在本地机器,如笔记本,上测试的时候。但当程序运行在一个远程的服务器上,性能是很重要的,因此不能使用这个标志。

你可以看一下 –run_locally 这个名字是用来干什么的,但它有一些问题:

  • 一个新成员不知道它是用来干什么的。他可能在本地运行的时候使用它(假设),但他不知道为什么需要这样做。
  • 偶尔,我们需要打印调试信息,但程序运行在远端。给远端运行的程序传递 –run_locally 看起来很滑稽,并会引起混淆。
  • 有时,我们想要在本地运行性能测试,且不想记录降低运行速度,因此我们不能使用 –run_locally

问题是, –run_locally 通常用于它的名字所表示的情况下。作为替代,像 –extra_logging 这样的标志名应该是更直接和清楚的。

但是,如果 –run_locally 需要做的不仅仅是额外的日志记录?例如,假设需要建立和使用一个特殊的本地数据库。现在 –run_locally 这个名字看起来更吸引人,因为对这两个方面它都可以控制。

但是使用它的目的是要挑选一个含糊的和不直接的名字,这可能不是一个好的主意。一个更好的解决方案是产生一个名叫 –use_local_database 的第二个标志。纵然现在,你不得不使用两个标志,但是这两个标志会更清楚;它们不会尝试去打破两个正交的概念而融为一体,并且它们给你只使用一个,而不用另一个的选择。

2.4. 附加额外的信息到一个名字

_images/2-3.png

正如我们前面所提及的,一个变量名如同一个微小的注释。即使没有没有太多的空间,任何被你挤入名字的额外信息,在每次变量出现的时候都可以被看见。

因此,如果关于变量有什么非常重要的事情,读者必须知道的话,附加一个额外的“字”到名字是值得的。例如,假设你有一个变量包含一个十六进制的字符窜:

string id;  // 例:"af84ef845cd8"

2.4.1. 带有单位的值

如果你的变量是一个度量(例如时间的总额或字节数),把单位编码到变量名是有帮助的。

例如,这是一个 JavaScript 的代码,测量一个网页的加载时间:

var start = (new Date()).getTime(); // top of the page
...
var elapsed = (new Date()).getTime() - start; // bottom of the page
document.writeln("Load time was: " + elapsed + " seconds");

这一代码并没有什么明显的错误,但它不能工作,因为 getTime() 返回毫秒,而不是秒。

通过追加 _ms 到变量,我们可以使得一切变得清晰:

var start_ms = (new Date()).getTime(); // top of the page
...
var elapsed_ms = (new Date()).getTime() - start_ms; // bottom of the page
document.writeln("Load time was: " + elapsed_ms / 1000 + " seconds");

除了时间之外,在程序中会出现大量其它的单位。下面是一个函数表格,其参数没有单位,且提供了一个包含单位的更好的版本:

函数参数 重命名参数来编码单位
Start(int delay ) delay -> delay_secs
CreateCache(int size ) size -> size_mb
ThrottleDownload(float limit ) limit -> max_kbps
Rotate(float angle ) angle -> degress_cw

2.4.2. 编码其它的属性

附加信息到名字的技术限制于带有单位的值。无论何时,变量有一些危险或奇怪,都可以使用这一点。

例如,许多安全漏洞来自于没有意识到你的程序接受到的数据尚未处于安全状态。对于这一点,你应该想使用像 untrustedUrlunsafeMessageBody 这样的名字。在调用函数来清除不安全的输入后,结果变量可能是 trustedUrlsafeMessageBody

下面的表格显示了额外的示例,什么时候应该把额外的信息编码到名字:

情形 变量名 更好的名字
一个明文的密码,在进一步处理之前应该加密 password plaintext_password
用户提供的评论在显示前需要转义 comment unescaped_comment
html的字节应该被转换为UTF-8 html html_utf8
输入的数据已经被“url 编码” data data_urlenc

你不应该在你的程序中对 每个 变量使用像 unescaped__utf8 这样的属性。在bug容易潜行的地方它们是非常重要的,如果有人错误的理解了变量的含义,尤其在极端的情况下,会产生一个安全漏洞。从本质上讲,如果一个事情是理解的关键,那么就把它放在名字里。

这是匈牙利命名法吗?

匈牙利命名法 是一个系统的命名法,在微软内部广泛使用。它每一个变量的“类型”都编码到名字的前缀。这里是一些例子:

名字 意思
pLast 一个指向某个数据结构最后一个元素的指针(p)
pszBuffer 一个指向一个零终止(z)字符串(s)缓冲区的指针(p)
cch 字符(ch)的计数(c)
mpcopx A map (m) from a pointer to a color (pco) to a pointer to an x-axis length (px)

这确实是一个“把属性包装到名字”的例子。但是,这是一个过于形式和严厉的系统,专注于编码一组特别的属性。

本节我们所倡导的是一种更广阔的,更通俗的系统:识别变量的任何重要的属性,如果需要的话,清晰的编码它们。你可以叫它“英语标记发”。

2.5. 一个名字应该有多长?

_images/2-4.png

当包装一个好的名字时,有一个暗含的约束:名字不能太长。没有人愿意使用这样的标识符:

newNavigationControllerWrappingViewControllerForDataSourceOfClass

名字越长就越难记忆,并它会占据更多的屏幕空间,可能导致额外的换行。

另一方面,程序员可能太过于使用这一建议,使用仅仅一个单词(或一个字母)的名字。因此,你如何把握这一平衡?你如何在命名 d, days, 或 days_since_last_update 间做决定?

这个决定是一个判断:谁更好的回答了变量是如何被使用的。这里有一些准则,以帮你做决定。

2.5.1. 较短的名字对于较短的作用域是可以的

当你有一个短假期时,较于长假期你会打包更少的行李。类似的,有一个小的“作用域”(有多少行代码可以“看见”这个名字)的标识符,不需要承载太多信息。也就是说,你可以使用较短的名字,因为所有的信息(变量是什么类型,其初始值,她是如何别销毁的)很容易的被看到:

if (debug) {
    map<string,int> m;
    LookUpNamesNumbers(&m);
    Print(m);
}

即使 m 并没有包装任何信息,也不是一个问题,因为读者已经有了她需要理解代码的所有信息。

然而,假设 m 是类的一个成员或一个全局变量,你只会看到这个代码片段:

LookUpNamesNumbers(&m);
Print(m);

这个代码比较难读,因为它不清楚 m 的类型的目的。

因此,如果一个标识符有一个较大的作用域,名字需要承载足够的信息使它清晰。

2.5.2. 键入长的名字——不再是一个问题

有许多避免使用长名字的原因,但“它们难以输入”已不再是其中之一了。我们见到的每个程序文本编辑器都内置了“单词补全的功能。令人惊讶的是,大多数程序员并不清楚这个特性。如果你还没有在你的编辑器上使用这一特性,请现在放下本书并去试用一下:

  1. 键入名字的前几个单词。
  2. 触发单词补全命令(见下面)。
  3. 如果补全的单词不正确,持续触发这一命令直到正确的名字出现。

它惊人的准确。它适用于任何类型和语言的文件。并可在任何地方使用,即使是在键入注释的时候。

编辑器 命令
Vi Ctrl-p
Emace Meta-/ (hit ESC, then /)
Eclipse Alt-/
LntelliJ IDEA Alt-/
TextNate ESC

2.5.3. 字首组合词和缩写

程序员有些时候使用字首组合词和缩写的手段来使他们的名字短小——例如,使用 BEManger 代替 BackEndManager 命名一个类。对于潜在的混淆,这一收缩是值得的吗?

以我们的经验,项目特定的缩写通常是一个坏的主意。它们对项目的新成员是隐蔽的和恐吓的。如果时间足够长的话,它们甚至对作者也是隐蔽的和恐吓的。

因此,我们的经验法则是:新队友会理解名字的含义吗?如果是的话,那么可能是一个好的命名。

例如,对于程序员来说,使用 eval 代替 evaluationdoc 代替 document , str 代替 string 是非常通用的。因此,一个新的队员看见 FormatStr() 应该知道它的意思。然而,他或她也许并不知道 BEManager 是什么。

2.5.4. 对掉不需要的词

有些时候可以去掉名字里的一些词,并不丢失任何信息。例如,作为 ConvertToString() 的替代,ToString() 会更短小并不会丢失任何真正的信息。类似的,代替 DoServeLoop() ,名字 ServeLoop() 会更清晰。

2.6. 利用名字格式化来传达含义

使用下划线,破折号和首字母大写的方式,可以在名字里包装更多的信息。例如,这里有一些C++代码,它们是依照 Google 开源项目格式化规范

static const int kMaxOpenFiles = 100;

class LogReader {
public:
    void OpenFile(string local_file);
private:
    int offset_;
    DISALLOW_COPY_AND_ASSIGN(LogReader);
};

不同的实体有不同的格式,就像一个代码高亮的形式一样——它可以帮助你容易的阅读代码。

在这个例子里的大多数格式是非常通用的——对类名使用 驼峰法 对变量使用 lower_separated 。但其它的一些约定可能会使你感到惊讶。

例如,常量值使用 kConstantName 代替 CONSTANT_NAMAE 。这种风格能很好的区分 #define 宏的形式,它依照约定是 MACR)_NAME

类成员变量像普通变量一样,但必须用下划线结束,如 0ffset_ 。起初,这一约定可能看起来比较奇怪,但它能很便利的把成员变量和其它变量区分开。例如,你在浏览一个大的方法的代码,看见了这一行:

stats.clear()a;

你通常不知道, stats 属于该类吗? 这一代码是否改变类的内部状态。如果使用 member_ 这一约定,你可以很快的推断,不, stats 必定是一个局部变量,否则它会被命名为 stats_

2.6.1. 其它格式化约定

取决与你的项目或语言的背景,可能有其它的格式化约定,你可是使用来使名字包含更多的信息。

例如,在 JavaScript: The Good Parts (Douglas Crockford, O’Reilly, 2008 一书中,作者建议“构造器”(旨在被 new 调用的函数)应该首字母大写,且其它普通函数应该首字母小写:

var x = new DatePicker(); // DatePicker() is a "constructor" function
var y = pageHeight();     // pageHeight() is an ordinary function

这有另一个JavaScript的例子:当调用jQuery库函数(它的名字是一个但字母 $ )时,一个有用的约定是给返回值也加上 $ 的前缀:

var $all_images = $("img"); // $all_images is a jQuery object
var height = 250;           // height is not

在代码的各处,将会是清楚的,$all_images 是一个jQuery返回值对象。

这一次是一个关于 HTML/CSS 的好的例子:当给HTML标签一个id或class属性时,在值中下划线和破折号都是有效的字符。一个合适的约定是使用下划线来分割ID标签的单词,使用破折号分割class标签的单词:

<div id="middle_column" class="main-content"> ...

你决定是否使用这些约定取决于你和你的团队。但不论市容何种,在你的项目中应该保持一致。

2.7. 小结

本章的主题是: 包装信息到你的名字 。通过这一点,我们的意思是,读者可以仅通过阅读名字就可以提取许多信息。

下面是我们讨论过的一些具体的提示:

  • 使用特定的词—— 例如,替代 Get ,更具上下文环境,使用像 FetchDownload 这样的次会更好。
  • 避免通用的名字 ,如 tmpretval ,除非有特殊的原因使用它们。
  • 使用具体的名字 ,更详细的描述事情——如,给表示毫秒值的变量后面加上 _ms 或给需要转义的变量前面加上 raw_
  • 作用域越大名字越长 ——不要对跨越好几屏的变量使用含糊的或两个字母名字;短小的名字适合于那些只跨越几行的变量。
  • 使用首字母大写,下划线,以及像这样的有意义的放肆 ——例如,你可以对类成员加上”_”来区别于那些局部变量。