编写易读代码的艺术——第二章 把精确包含到名字里
2012-03-09 14:39
232 查看
把精确包含到名字里
无论是为变量,函数,或累命名,已经有很多技巧可以使用了。在这一章,我们主张把其中一个技巧重要性放到最前:把精确包含到名字里。我们看到的程序中,很多名字是很模糊的,如tmp。甚至那些看上去有意义的词,如size或get, 没什么精确性可言。本节就是要展示给你如何去选那些具体而又直接的词。
选择明确的词
“把精确包含到名字”的部分意义就是选择那些非常明确的词,避免空洞的词语。例如,“get"就不是一个非常明确的词,像下面的例子:
def GetPage(url): …
词”get"能表明的不是很多。这个方法是从本地缓存获取一个页面,还是从数据库里,又或者是从互联网中?如果是从网站上,那么Fetch或Download可能更合适。
又如下面BinaryTree的例子:
class BinaryTree { int Size() { … } … };
你期待size()返回什么?树的高度,节点数,还是树的内存使用量?
问题的原因是size没有精确性。一个更准确的名字是height(), NumNodes(), 或MemoryBytes()。
另一个例子:假设你有一个Process类:
class Process { void Stop(); … };
Stop这个词还行,但鉴于这个函数所作的事,也许有更合适的名字。例如,你可以命名为kill(),如果这是一个无法恢复的操作。又或者你可以命名为Pause(),如果他有一个resume()的方法。
找更“生动”的词
不要怕使用同义词典,或问朋友一个更好的名字建议。英语是一门丰富的语言, 有很多可选的词汇。
以下是一些词汇的例子,也许更“生动”的版本可以应用到你的情况里:
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 |
程序员会因为名字的精确性和明白性,而不是创造性而加分。
示例:earth_distance()
这是一个根据经度和纬度计算地球上两点间的距离的函数:def earth_distance(lat1, lng1, lat2, lng2): …
这是一个名字没包含精确性的方法的例子。有人看到这个方法的调用时很容易就会想:
它是返回千米,英里,还是其他?
它返回的是车程,球面距离,还是直线距离?
从而,一个好名字会包含足够的信息回答以上问题。下面是一些可选的的更精确的名字:
arc_length_miles()
arc_miles()
Generic Names Like tmp, it, and retval
像foo, blah, 或者 num 这些名字都是“我想不到一个名字”的借口。与其用这些毫无意义的名字,不如选一个能描述实际值或目的的名字。例如,这是个使用num的方法:
function EuclideanNorm(v) { var num = 0.0; for (int i = 0; i < v.length; i++) num += v[i] * v[i]; return Math.sqrt(num); }
num表示一个数字没错,但是他装得到底是什么值?他的目的是什么?这个变量是V的平方的累计值。因此一个更好的名字是sum_squares。这个名字直白地表明了这个变量的意义,可能还能帮助找到一个bug。
例如,如果循环里面不小心写了:
num += v[i];
这个bug会更明显如果代码如下:
sum_squares += v[i]; // Where's the "square" that we're summing? Bug?
然而,在某些情况下,比较泛的名字的确会有意义。让我们看看什么时候使用它们是可以的。
tmp and temp
有时一个变量仅仅是个临时值——用来存储一个值然后很快就被毁掉。典型的示例是是swap()方法:swap(ref Object a, ref Object b) { tmp = a; a = b; b = tmp; }
在这种情况下,tmp (或temp) 已经完全足够,因为没更多的信息要表达了。这个变量的角色就是一个临时的存储器,声明周期就是那几行代码。一个包含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是一个文件,一个文件名,或甚至只是一个被重写的变量。
Loop iterators
名字如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 < people.size(); k++) if (clubs[i].members[k] == people[j]) cout << "people[" << j << "] is in club[" << i << "]" << endl;
在上面的if语句里,members[]和people[]使用了错的索引。这样的bug是很难定位的因为你必须同时追踪所有的变量。每一行代码单独看来都没什么错误。
这种情况下,使用更精确的名字是有帮助的。对于循环索引像(i,j,k), 另一个选择是 (
club_i,
members_i,
people_i),或更简单一点(ci,mi,pi)。这样就能是bug更容易被发现:
if (clubs[ci].members[pi] == people[mi]) # Bug! First letters don't match up
正确的是,索引的第一个字母与数组变量的第一个字母应该是相同的:
if (clubs[ci].members[mi] == people[pi]) # OK. First letters match.
Using retval
retval(or ret)像tmp,是另一种变量,精确的说明一件事。retval说,“我是个返回值”。不幸的是,这个信息没什么用.int retval = ParseImage(…);
很明显,retval是一个方法的返回值,所以这个名字并不非常精确。如果变量是为了抓住一个exit_status,或一个error_code,那么以上就是一些比较好的名字。
retval有时是用来建立或计算一个方法的返回值:
// Returns a string like ['<id1>' , '<id2>' , … , ] string IdList::Serialize() { string retval = "["; for (int i = 0; i < ids.size(); i++) { retval += Format("'%d' ,", ids[i]); } retval += "]"; return retval; }
又一次,作为一个“返回值”并不是什么重要的事。一个更重要的事实是这个程序员意图是返回一个有JSON格式的值。这种情况下,一个更好的名字是json。把注意力吸引到这件事上能帮助找到bug。(格式正确的JSON,你不能使用单引号,你不能有多余的逗号在句尾)。
The verdict on generic names
总的来说,知道什么时候使用tmp,it,或retval这些名字是很容易的:如果你要使用它们,你最好有一个使用它们的理由。很多时候使用它们是因为懒惰——当想不到更好的名字的时候,用一个无意义的名字像foo比较容易。但是如果你养成了多花几秒钟去想个好名字的习惯,你就会发现你的“命名肌肉”会生长得很快。使用具体名称而不是抽象名称
当为一个变量,方法等命名时,用具体的描述,而不是抽象的。例如,假设你有一个内部方法叫ServerCanStart(),是用来测试服务器是否能监听已有的TCP/TP端口ServerCanStart()这个名字有些抽象。因此CanListenOnPort()这个名字更具体的描述了这个方法做的事。
下面两个例子进一步说明这个概念。
Example:DISALLOW_EVIL_CONSTRUCTIONS
这个例子来自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_CONSTRUTORS并不是个好名字。“evil”这个词很强烈的说明了一个备受争论的问题。但更重要的是,它meishuoming宏禁止了什么。实际上,它禁止了operator=()方法,即使他不算是一个“构造器”!
这个名字使用了很多年,但最近改成了一个更少争议更具体的名字:
#define DISALLOW_COPY_AND_ASSIGN(ClassName) [...]
Example: ——run_locally
我们其中的一个程序有一个可选的命令行选项叫——run_locally。这个标志位会导致程序打印额外的调试信息,是程序运行变慢。这个标志位特别用于在本地机子测试的时候,如笔记本电脑。但是程序运行在远程服务器上时,效率是重要的,因此这个标志位不会被使用。你可以看出——run_locall是怎么来的,但是会引出一些问题:
一个新加入的组员不知道他是干嘛的。你肯能会在本地运行时使用这个标志位,但是他不知道这个标志位的作用或为什么需要他。
有时候,我们需要看一些调试信息当程序远程运行的时候。设置——run_locally事,程序却在远程运行,看起来既有些可笑又令人疑惑。
又有时,我们需要测试本地运行的效率,不希望看到日志使它变慢,所以不会设置——run_locally。
问题的原因是run_locally是根据特定情况下的特别用法命名的。反而——extra_logging会更直白清楚。
但是若run_locally做的不只是增加额外的日志呢?例如,它意味着需要建立一个特别的本地数据库?这样,看起来更倾向于run_locally这个名字,因为他能同时包含以上两个状况。
但是那样使用会导致名字即模糊又不直截,这不是个好主意。更好的办法是再设置一般标志位叫——use_local_database。即使你需要使用两个标志位,但这些标志位更准确,因为他们没有把两个毫不相关的概念硬融合到一起,并且他们给你只使用其中任意一个的选择。
给名字加上额外信息
某种意义上来说,一个变量名就像一段小的注释。尽管字数不多,但你要表达任何额外信息一旦加入到名字里,每次看到这个变量时这些信息都能被看到。因此,如果有一些关于这个变量的重要信息读者必须知道,在名字里加一个额外的“词”是值得的。例如,我们与其定义一个变量名:
string id; // e.g. "af84ef845cd8"
不如命名为hex_id,如果这个id的格式对读者来说比较重要。
带单位的值
如果你的变量是测量单位(如时间或字节数), 那么把单位加到名字里是有益的。例如,这个一段测试网页加载时间的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");
除了时间,在编程是还会遇到很多其他的单位。下表一边是不带单位的参数的方法,一边是带单位的更好的版本:
Function Parameter | Renaming parameter to encode units |
---|---|
Start(int delay ) | delay → delay_secs |
CreateCache(int size ) | size → size_mb |
ThrottleDownload(float limit ) | limit → max_kbps |
Rotate(float angle ) | angle → degrees_cw |
AdjustMotorSpeed(float spin_rate ) | spin_rate → rpm |
给名字加上重要的属性信息
给名字带上额外信息不只有把单位加到名字这个技巧。通常来说,你需要使用这个技巧当你的变量比较危险的时候。例如,很多安全缺陷是因为你没意识到你的程序接收的数据是不安全的。这种情况下,你可以使用变量名像untrustedUrl或者unsafeMessageBosy。再调用那些清除不安全输入的方法后,结果变量可以是trustedUrl或safeMessageBody。
下表展示了一些需要添加额外属性信息的名字的例子:
Variable Name | Situation | Better name(s) |
---|---|---|
password | the password is “plaintext” and should be encrypted before further processing | unencrypted_ password password _plaintext |
comment | a user-provided comment that needs escaping before being displayed | raw_ comment unescaped_ comment |
html | the bytes of htmlhave been converted to utf-8 | html _utf8 |
data | incoming data has been “url encoded” | data _urlenc |
这是匈牙利命名法么?
匈牙利命名法是一种在微软里使用广泛的命名系统. 它把所以变量的类型都放到名字的前缀. 下面就是一些例子:
Name | Meaning |
---|---|
pLast | a pointer (p) to the last element in some data structure |
pszBuffer | a pointer (p) to a zero-terminated (z) string (s) buffer |
cch | a count (c) of characters (ch) |
mpcopx | a map (m) from a pointer to a color (pco) to a pointer to an x-axis length (px) |
我们这里提倡的是一种更广泛,非正式的系统方法:识别每一个变量的重要属性,如果需要的话,以更易读的形式加到名字中。你可以叫这个方法为“英语命名法”。
名字应该多长?
当选一个好名字时,有一个隐性限制就是名字不能过长。没人喜欢名字像:newNavigationControllerWrappingViewControllerForDataSourceOfClass.
名字越长,越难被记住,占的屏幕空间越大,很可能导致要换行。
另一方面,程序员又会变得极端而只使用单个词的名字。所以,你如何在两者中取舍呢?你如何决定是使用d,days还是days_since_last_update?
这个决定取决于到底这个变量是如何使用的。如下一些指导能帮助你做决定。
短的名字适合短的作用域
当你修短假的时候,你准备的行李肯定比你休长假的时候要少。同样,作用域小的标示符,也不需要带太多的信息。也就是说,你使用短名字是因为其他信息比较容易被看到:if (debug) { map<string,int> m; LookUpNamesNumbers(&m); Print(m); }
即使m没包含什么信息,对读者理解这段代码并没有造成什么困难。一个长的名字反而会让人分心,因为所有的信息都在那3行代码里。
然而,如果m是一个类成员或一个全局变量,看看下面的代码:
LookUpNamesNumbers(&m); Print(m);
这段代码的可读性就不是那么好了,因为不清楚m是什么类型或什么意义。所有,如果标示符的作用域很大,那么名字就要有足够的信息让人明白。
敲长名字——不再是个问题
避免使用长名字有很多理由,但是“难敲”不再是其中一个。我们见过的每个程序编辑器都有自动填充的功能。但令人惊讶的是,大多数程序员并没有注意到这个功能。如果你还没在你的编辑器试过,那么请记下并立马尝试。下面的表说明得很详细.
你的编辑器包含自动填充功能!
下次你要输入一个长名字,请尝试以下步骤:
键入开头几个字母.
启动自动填充命令(看下面).
如果填充的词不正确,一直按着自动填充命令,直到正确的词出现。
他是如此令人惊讶的精确。他可以用于任意的言语,任意类型的文件中。并且他可以用于任意的标记,甚至是在注释中
Editor | Command |
Vi | Ctrl-P |
Emacs | Meta-/ (hit ESC, then /) |
Eclipse | Alt-/ |
Visual Studio | Ctrl-space |
IntelliJ IDEA | Ctrl-(shift?)-space |
头字母和缩写
程序员有时候使用首字母和缩写来使名字简短。例如,把类命名为BEManager而不是BackEndManager。这些缩减能抵消带来的潜在的问题么?在我们的经验里,项目相关的缩略测是个坏主意。他们让新加入到项目中的人感到困惑和畏惧。时间够长的话,作者自身也会感到困惑和不安。
所以,我们的命名经验是:新来的组员能理解这个名字么?如果能,那么,这个名字就是ok的。
例如,像程序员常用的是eval而不是evaluation,doc而不是document,str而不是string。所以,当一个新成员看到FormatStr()很可能会理解这个名字的意思。然而,他们就不大可能理解BEManger是什么了。
去掉不需要的词
有时候,去掉名字里的一些词根本不会丢失这个名字所携带的信息。例如,相对于使用ConvertToString(),ToString()不但简单,而且不会丢失任何信息。同样,ServeLoop()相对于DoServerLoop()来说,已经足够明白。用名字的的格式来表达意义
你使用下划线,破折号,和大写字母的方式也可以使名字包含更多信息。例如,下面是Google使用C++代码时遵循的格式惯例:static const int kBufferSize = 4096; class LogReader { public: void OpenFile(string local_file); private: int offset_; DISALLOW_COPY_AND_ASSIGN(LogReader); };
不同的格式应用于不同的实体就像语法高亮一样——能帮助你更容易的编程。
上面使用很多格式是很常用的——对类使用驼峰命(CamelCase)名法,对变量使用lower_separated。但有些惯例会让你吓一跳。
例如,对常量使用kConstantName而不是CONSTANT_NAME。这样的好处是容易与#define宏区分开来,因为宏一值都是使用MACRO_NAME.
类成员变量跟一般变量一样,但必须以下划线结束,如offset_。这一开始看起来会感到奇怪,但随之而来的好处是能立马把类成员和一般变量区分开来。例如,你正在扫描一个很长的方法,看到一行代码:
stats.clear();
你就会想,“这个stats是属于类的么?这段代码是否修改类的状态?”如果使用的是member_惯例,你就能立马得到结论,“不是,stats应该是个局部变量。否则,他应该是stats_”。
其他命名格式
根据你项目的背景或使用的语言,还有其他的命名格式可以使用来给名字添加更多信息。例如,在《Javascript:好的部分》,作者建议构造器(用new来调用的方法)应该以大写字母开头,而其他方法应该以小写字母开头:
var x = new DatePicker(); // DatePicker is a "constructor" function var y = reloadPage(); // reloadPage 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"> …
你是否使用以上惯例取决于你或你的小组的决定。但无论你使用怎样的规则,请在项目中保持一致性。
总结
这一章的唯一主题是:把精确性包含到你的名字中。 这句话的意思是,读者能从读你代码的名字中,就能获取很多信息。下面就是我们如何做到这样:
使用特定的词——例如,相对于使用Get, Fetch或Download会更好,取决于上下文。
避免空泛的名字如num,tmp,或retval除非你有特别的理由要使用它们。
取具体的名字,使它们能提供更多的细节——ServerCanStart()比CanListenOnPort()的意思模糊
提供更多细节给变量名——如,把_ms加到存储毫秒值变量名中,把raw_加到未处理的需要转义的变量前。
以有意义的方式使用大写字母,下划线等——如你可以把”_“加到类成员中与其他变量区别开来。
同时,你希望你的名字的长度符合它的作用域。如,不要对那些作用域横跨几个屏幕的变量使用含义模糊的或两个字母的名字。也没有必要对只有几行代码作用域的变量使用过长的名字。
相关文章推荐
- 编写易读代码的艺术——第三章 名字应不能被误解
- 编写可读代码的艺术读书笔记--把信息装到名字里
- 编写可读代码的艺术(三)不要起误解的名字以及代码上的‘审美’
- 编写易读代码的艺术——第一章 代码应该容易让人理解
- 编写易读代码的艺术——第四章 美学
- 读书笔记-编写可读代码的艺术[中]
- 编写可读代码的艺术之一
- 编写可读代码的艺术 读后感(三)
- 易读代码的艺术之Packing Information into Names 1
- IIS7 支持html页面包含(include)html页面 IIS设置与代码编写
- 编写可读代码的艺术读书整理
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 避免使用终结器,避免大对象,避免复制缓冲区
- 编写可读代码的艺术
- 第二章: 编写Pascal代码
- 代码编写就像一门艺术
- 读书笔记-编写可读代码的艺术[下]
- 编写可读代码的艺术
- PHP如何编写易读的代码
- 编写易读的代码
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 将长生命周期对象和大对象池化