编写易读代码的艺术——第三章 名字应不能被误解

当为变量,方法,类或其他命名时,你希望下一个读你代码人能完全理解你的意思。最坏的情况是他人把你取的名字理解成与你所想的完全不一样的意思。

在这一章,我们将给你展示这种状况是怎样发生的,然后怎样避免它。

最基本的思想是:主动仔细检查你取的名字,问你自己,“别人会把这个名字理解成其他什么意思呢?”这要求你必须要有创造性,积极寻找“错误的理解”。这能帮助你定位那些意义模糊的名字并修改他们。

这章的每一个例子,我们会像往常一样“边想边说”,详细的列出我们看到的关于名字的所有误解,然后跳出更好的名字。

Example: Filter()

假设你写了一些代码来处理数据库的结果集:

results = Database.all_objects.filter("year <= 2011")

result现在包含什么数据呢?
  • 所有年限<=2011的数据?
  • 所有年限不<=2011的数据?

问题的原因是filter是个模糊的词。不清楚它是指“挑出”或“去掉”。所以最好避免使用filter因为它容易造成误解。

如果你的意思是“挑出”, 一个更好的名字是select()。如果你的意思是“去掉”,那么更好的名字是exclude()。

Example:Clip(text,length)

假设你有一个从内容中接剪切一段话的方法:

# Cuts off the end of the text, and appends "..."
def Clip(text, length):
  …

你可以看出Clip()的行为有两种理解方式:

  • 他移去结尾length的内容
  • 他只获取length的内容

很有可能是第二种方式,但是你不能肯定。与其让你的读者烦恼,不如把方法命为Truncate(text,length)。

然而,参数length的名字也必须负一部分责任。如果是max_length,那么就更容易理解了。

但是这还没完。max_length仍然可以有多重解释:

  • 字节大小
  • 字母个数
  • 单词个数

如我们在上一章所说的,这时就需要把单位信息加到名字里。这里,我们指的是”字母个数“,所以不是max_length,而是max_chars。

为闭区间的限制使用min和max

又假设你的购物车必须禁止一次购买超过10件以上的商品:

CART_TOO_BIG_LIMIT = 10

if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
  Error("Too many items in cart.")

这段代码有一个经典的大小差一的bug。我们很容易通过把>=改成>来修正:

if shopping_cart.num_items() > CART_TOO_BIG_LIMIT:

(或者把CART_TOO_BIG_LIMIT改成11)。但是根本问题是CART_TOO_BIG_LIMIT是个模棱两可的名字——不清楚它是指”小于“还是”小于并包含“。

最清楚的方法是把max_和min_放到限制变量的名字前面。在这个例子里,名字应该为MAX_ITEMS_IN_CART。

最终结果就变得简单清晰:

MAX_ITEMS_IN_CART = 10

if shopping_cart.num_items() > MAX_ITEMS_IN_CART:
  Error("Too many items in cart.")

max不止是一个没有歧义的词,并且它只有3个字母,在代码中应用广泛。

对闭区间使用first和last

这是另一个你不知道是”小于“还是”小于等于“的例子:

print integer_range(start=2, stop=4)
# Does this print [2,3] or [2,3,4] (or something else)?

尽管start是个合理的参数名,stop仍然会有不同的解释。

对于像这样闭区间(即所指范围区间包括开始和结束点),first/last是个好选择。例如:

set.PrintKeys(first="Bart", last="Maggie")

不像stop,last很明显指的是闭区间。

除了first/last, min/max对闭区间也可以使用。

对半开半闭区间使用begin和end

在实践中,半开闭区间使用得更频繁。例如,你想打印10月16号前发生的所有事件,这样写:

PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")

比这样写更容易:

PrintEventsInRange("OCT 16 12:00am", "OCT 16 11:59:59.9999pm")

所以,什么样的参数名会比较好?比较典型的对半开闭区间命名的编程惯例是使用begin/end。

但是end是有点模糊地。例如,在句子,”I'm at the end of the book“, ”end“是闭区间的。不幸的是,英语对”刚好大于最大值的值“没有一个简洁的词。

然而begin/end是如此普遍的被使用,使它成为了最好的选择。

为布尔变量取名

当为一个布尔变量或返回布尔值的方法取名是,要说清楚true和false到底指的是什么意思。

这是个危险的例子:

bool read_password = true;

根据你看到的,有两种不同的解释:

  • 我们必须读取密码
  • 密码已经被读取了

在这个例子中,最好避免使用”read“, 而是用need_password或者user_is_authenticated。

这是另一个例子,对一个已经把标价发送到一个拍卖,并得到回复的系统:

empty_bid_response = …

名字empty_bid_response可以有多种解释。甚至不能肯定empty_bid_reponse是布尔变量还是一个对象。假设它是个布尔变量,empty_bid_reponse=true仍然有不止一种解释:

  • 报价的回复是空的
  • 报价的回复在后面应该被清空(可能是为了释放内存)
  • 一个空的报价得到了一个成功的回复

根据后面的代码,也许有的解释根本不合理,但是为什么必须要让自己读者去弄懂呢?一个更好的名字bid_response_is_empty就能说明了第一种状况。

通常,使用is,has,can,或should就能是布尔变量更清楚。如名为SpaceLeft()的方法返回一个布尔变量会让人困惑,但HasSpaceLeft()可能就不会。

最后,最好对变量避免使用否定词。例如,与其

bool disable_ssl = false;

不如:

bool use_ssl = true;

配合用户的期望

有些名字会有误导因为用户对使用的名字已经有先见的固定的解释,尽管你说的是另外的东西。这种状况下,最好“放弃”并把名字改成没有误导性的,

Example:get_*()

很多程序员已经习惯地认为以get_开头的方法是一个返回内部成员的“轻量级访问器“。所以违反这个规则会误导用户:

这是个不要做什么的例子:

public class StatisticsCollector {
  public void addSample(double x) { … }

  public double getMean() {
    // Iterate through all samples and return total / num_samples
  }
  …
}

在这个例子中,getMean()的实现是遍历所有的数据并同时计算平均值。这会花费许多时间如果数据太大。但是一个没有疑心的程序员会可能会随意的调用这个方法,认为这个调用花费的时间很小。

所有,这个方法应重命名为computeMean(),听起来才更像一个开销比较大的操作。

Example:list::size()

这是一个在C++标准库宏的例子。这段代码导致了一个极度难发现的使我们的服务器慢得像爬的一样的bug:

void ShrinkList(list<Node>& list, int max_size) {
  while (list.size() > max_size) {
    FreeNode(list.back());
    list.pop_back();
  }
}

导致“bug”的原因是作者不知道list.size()是一个O(n)的操作——它把链接点从头到尾的数一遍而不是直接返回一个值。所以ShrinkList()是一个O(n2)的操作。

这段代码看起来是“正确的”,实际上,也通过了我们所有的单元测试。但当ShrinkList()被一个百万级的列表调用时,花了一个小时才运行完!

你可能会想,“那是调用者的失误——他们应该更仔细的去读文档”。这话没错,但这个例子,list.size()不是一个常量时间的操作是很令人吃惊的。所有其他C++容器都有一个常量时间的size()方法。

如果这个size()方法的名字是countSize()或者countElement(),那么这个错误很可能就不会发生。C++标准库的作者们可能想把这个方法命名为size()使它能符合其他容器像vector或map。但是这样做后,程序员们很容易认为它是一个运行快的操作,像其他容器一样。不过值得感谢的是,在最新的C++0x标准中,要求size()操作必须是O(1)。

Example:为一个新的配置参数命名

大流量的网站经常使用“实验变量”来测试一个修改是否能网站的业务得到增长。这个是一个控制一些实验变量的配置文件的例子:

experiment_id: 100
description: "increase font size to 14pt"
traffic_fraction: 5%
…

每次实验都是定义15对值。不幸的是,当定义另一个相近的实验时,你必须复制粘贴很多行:

experiment_id: 101
description: "increase font size to 13pt"
[other lines identical to experiment_id 100]

假设我们想引进一个方法,一个实验能重用另一个实验的东西。最后的结果是你必须写些如下的东西:

experiment_id: 101
the_other_experiment_id_I_want_to_reuse: 100
… change any properties as needed

问题是:the_other_experiment_id_I_want_to_reuse的真正名字应该是什么?

有如下四个名字可以考虑:

  • template
  • reuse
  • copy
  • inherit

对于这个新增的特性,上面几个名字对我们来说都是可以的。但让我们考虑一下当不知道有这个特性的人看到代码时会有什么想法。所以让我们来逐个分析这些名字。想想别人会怎样误解它们。

让我们看看使用template:

experiment_id: 101
template: 100
…

template会导致一些问题。首先,不知道它是指“我是一个template”还是“我使用了这个其他的template”。其次,一个“template”是在具体化之前必须填一些东西的抽象的东西。有些人会认为一个templeted的实验不是一个“真的”使用。总之,template在这个例子中意义模糊。

那么reuse:

experiment_id: 101
reuse: 100
…

  reuse则一般般,但正如所写的,他人会想“这个实验最多能重用100次”。把名字改为resuse_id能有些帮助。但一个困惑的读者会想resuse_id:100意味着“我的重用的id是100”。

再看看copy:

experiment_id: 101
copy: 100
…

copy是个好词。但单独看起来,copy:100像是说“复制这个实验100次”或“这是某个东西的第100次复制”。为了使它更清晰表明这是引用另一个实验,我们可以把名字改成copy_experiment。这可能是目前为止最好的名字。

再来看inherit:

experiment_id: 101
inherit: 100
…

“inherit”这个词对多数程序员来说应该是很熟悉的了,很容易明白进一步的修改是在继承之后。在类继承中,你能活到另一个类的方法和成员,然后修改它们或增加它们。即使在现实生活中,当你继承了一个亲戚的财产,你可以出售或拥有其他你自己的的东西。

再次,我们要说的是我们继承了其他实验。我们能改进名字成inherit_from甚至inherit_from_id。

总的说来,copy_experiment和inherit_from_id是最好的两个名字,因为它们很清楚的说明了发生的事情,并不容易被误解。

总结

最好的名字是那些不会引起误解的名字——读你代码的人会以你想要的方式来例假,不会再有其他的理解方式。不幸的是,一到编程,很多英文单词都都会变得模棱两可,就像filter,length和limit。

但是挑选好的名字不仅仅是避免“坏”单词那么简单。在你决定一个名字之前,扮演一下恶魔的拥护者,想想你的名字会被怎样误解。好的名字经得起推敲。

但定义上下线值的时候,max_和min_是一个好的前缀。对于闭区间,first和last是好名字。对于半开闭区间,begin和end是好名字因为他们是最惯用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值