命名在编程中的重要性


我坚信编写代码与此类似:通过为函数、变量和其他构造物找到好的名称,我们真正认识到我们正在解决的问题的本质。清晰性带来的不仅仅是好的名称,还有更清晰的代码和改进的架构。

我会说,写出干净代码的90%其实就是“仅仅”是正确命名事物。听起来很简单,但实际上并非如此!

让我们来看几个例子。

例1

1
2
3
4
5
6
7
8
9
// Given first and last name of a person, returns the
// demographic statistics for all matching people.
async function demo (a, b) {
const c = await users(a, b);
return [
avg(c.map(a => a.info[0])),
median(c.map(a => a.info[1]))
];
}

这段代码存在哪些问题?

  • 函数 demo 的名称非常模糊:它可能代表 “拆除”,也可能是 “演示/展示”,等等。
    变量 a、b 和 c 的命名完全没有提供信息。
  • 在 map 内部,lambda 中重新使用了变量 a,遮蔽了作为函数参数的 a,使读者感到困惑,未来修改代码时更容易犯错,并引用错误的变量。
  • 返回的对象没有提供有关其包含内容的任何信息,相反,在以后使用时必须小心其元素的顺序。
  • users() 函数调用结果中字段 .info 的名称没有提供有关其包含内容的信息,这一点更糟糕的是,通过它们的位置访问其元素,也隐藏了有关它们的任何信息,使我们的代码容易在其顺序发生变化时悄悄出错。

让我们来修复它:

1
2
3
4
5
6
7
8
9
10
11
async function fetchDemographicStatsForFirstAndLastName (
firstName, lastName
) {
const users = await fetchUsersByFirstAndLastName(
firstName, lastName
);
return {
averageAge: avg(users.map(u => u.stats.age)),
medianSalary: median(users.map(u => u.stats.salary))
};
}

我们做了什么?

  • 函数的名称现在准确地反映了它的功能,不多也不少。名称中的 fetch 甚至表示它执行了一些输入/输出操作(在这种情况下是从数据库获取),这是值得知道的,因为与纯代码相比,IO 操作相对较慢/昂贵。
  • 我们确保其他名称提供了足够的信息:既不过多,也不过少。
    • 请注意我们使用了名称 users 作为获取的用户,而不是像 usersWithSpecifiedFirstAndLastName 或 fetchedUsers 这样更长的名称:没有必要使用更长的名称,因为这个变量是非常局部的,存在时间很短,周围有足够的上下文来清楚它是关于什么的。
  • 在 lambda 内部,我们使用了一个单字母名称 u,这可能看起来像是不好的做法。但在这里,它是完美的:这个变量的生存期非常短,从上下文中清楚地知道它代表什么。此外,我们特意选择了字母 u,因为它是 user 的第一个字母,因此使这种联系显而易见。
  • 我们为返回的对象中的值命名:averageAge 和 medianSalary。现在,任何使用我们的函数的代码都不需要依赖于结果中项目的顺序,并且阅读起来既简单又富有信息。

最后,请注意函数上方不再有注释。事实上,不再需要注释:从函数名称和参数中就足够清楚了!

例2

1
2
3
4
5
6
7
8
9
10
11
// Find a free machine and use it, or create a new machine
// if needed. Then on that machine, set up the new worker
// with the given Docker image and setup cmd. Finally,
// start executing a job on that worker and return its id.
async function getJobId (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}

在这个例子中,我们将忽略实现细节,只关注正确命名和参数。

这段代码有什么问题吗?

  • 函数名称隐藏了关于它正在执行的许多细节。它根本没有提到我们必须获取机器或设置工作人员,或者该函数将导致在后台某处继续执行的作业的创建。相反,它给人一种感觉,好像我们正在做一些简单的事情,这是由于动词 get:我们只是获取一个已经存在的作业的 id。想象一下在代码中的某个地方看到对这个函数的调用:getJobId(…) → 你不期望它花费很长时间或执行它实际上正在执行的所有工作,这是不好的。

好的,这听起来很容易解决,让我们给它一个更好的名称!

1
2
3
4
5
6
7
async function procureFreeMachineAndSetUpTheDockerWorkerThenStartExecutingTheJob (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}

唉,这是一个又长又复杂的名字。但事实上,如果我们不想失去有关这个函数的操作以及我们可以从中期望什么的有价值信息,我们实际上不能将其缩短。因此,我们陷入了困境,找不到更好的名称!现在怎么办?

问题在于,如果背后没有干净的代码,你就无法给出一个好的名字。因此,一个糟糕的名称不仅仅是一个命名错误,而且通常也是其背后代码有问题的指示,是设计的失败。代码如此问题重重,以至于你甚至不知道该如何命名它 → 没有直截了当的名字可供给它,因为它不是一个直截了当的代码!

在我们的情况下,问题在于这个函数一次性尝试做太多事情。长名称和许多参数是这一点的指标,尽管在某些情况下这些可能是可以接受的。更强烈的指标是名称中使用了 “and” 和 “then” 这样的词,以及参数名称可以通过前缀(machine、worker)分组。

解决方案是通过将函数拆分成多个较小的函数来清理代码:

1
2
3
async function procureFreeMachine (type, region) { ... }
async function setUpDockerWorker (machineId, dockerImage, setupCmd) { ... }
async function startExecutingJob (workerId, jobDescription) { ... }

什么是一个好的名字?

但让我们退后一步 - 什么是一个糟糕的名字,什么是一个好的名字?这意味着什么,我们如何辨别它们?

好的名字不会引导错误,不会省略信息,也不会假设。

一个好的名字应该让你对变量包含的内容或函数的作用有一个很好的想法。一个好的名字将告诉你所有你需要知道的,或者足以让你知道下一步应该去哪里查找。它不会让你猜测或纳闷。它不会误导你。一个好的名字是明显的,是符合预期的。它是一致的。不要过于创造性。它不会假设读者不太可能具有的上下文或知识。

此外,上下文至关重要:在读取的上下文中,你不能评估名称。verifyOrganizationChainCredentials 可能是一个糟糕的名字或一个好的名字。a 可能是一个好的名字或一个糟糕的名字。这取决于故事,环境,代码正在解决的问题。名称讲述一个故事,它们需要像一个故事一样配合。

一些著名的糟糕命名的例子:

  • JavaScript

    我自己曾是这种糟糕命名的受害者:我的父母给我买了一本有关 JavaScript 的书,而我想学习的是 Java。

  • HTTP Authorization header

    它被命名为 Authorization,但用于身份验证!而这两者并不相同:身份验证是关于确认身份,而授权是关于授予权限的。有关更多信息,请访问:https://stackoverflow.com/questions/30062024/why-is-the-http-header-for-authentication-called-authorization。

  • Wasp-lang:

    这是我的过错:Wasp 是一个使用自定义配置语言作为其代码库的一小部分的全栈 JS Web 框架,但我在名称中加入了 -lang,并吓跑了许多人,因为他们认为它是一种全新的通用编程语言!

如何想出一个好的名字

不要随意起名字,而是要找到名字
最好的建议可能是不要随意起名字,而是要找到一个名字。你不应该像给宠物或孩子起名字那样创造一个原创的名字;相反,你应该寻找你要命名的事物的本质,基于这个本质,名字应该自然而然地出现。如果你不喜欢你发现的名字,这意味着你不喜欢你要命名的东西,你应该通过改进代码的设计来改变这个东西(就像我们在示例#2中所做的那样)。

在确定名称时要注意的事项

  • 首先,确保它不是一个糟糕的名字 : )。记住:不要引导错误,不要省略,不要假设。

  • 确保它反映了它所代表的东西。找出它的本质,用名字捕捉它。名字仍然难看?改进代码。在这里,您还有其他一些工具可以帮助您 → 类型签名和注释。但这些是次要的。

  • 确保它与其周围的其他名称协调一致。它应该与它们有明确的关系 - 处于相同的“世界”中。它应该类似于类似的东西,相反于相反的东西。它应该与其周围的其他名称一起讲一个故事。它应该考虑到它所在的上下文。

  • 长度应符合范围。一般来说,名称的生命周期越短,范围越小,名称就越短/可以/应该,反之亦然。这就是为什么在短的 lambda 函数中使用单个字母变量可能是可以接受的。如果不确定,选择更长的名称。

  • 坚持在代码库中使用的术语。如果迄今为止使用了术语 server,就不要无故地开始使用术语 backend。同样,如果您使用 server 作为一个术语,您可能不应该选择 frontend:相反,您可能会希望使用 client,这是一个与 server 更密切相关的术语。

  • 坚持在代码库中使用的约定。以下是我在我的代码库中经常使用的一些约定的示例:

    • 对于变量为 Bool 时使用前缀(例如 isAuthEnabled)
    • 对于具有幂等性的函数使用前缀 ensure,该函数仅在到目前为止尚未设置时执行某些操作(例如 ensureServerIsRunning)。

    想出一个名字时的简单技巧

    如果你有困难想出一个名字,请按照以下步骤进行:

  • 在函数/变量上方编写一条注释,其中以人类语言描述它,就像你在向同事描述它一样。它可能是一句话或多句话。这是你的函数/变量所做的事情的本质,它是什么。

  • 现在,你扮演雕刻师的角色,通过削减和塑造对函数/变量的描述,直到得到一个名字。当你感到你的想象中的雕刻刀再次击打它时,你停下来,因为再打一次会夺走太多东西。

  • 你的名字是否仍然太复杂/令人困惑?如果是这样,这意味着背后的代码太复杂,应该进行重新组织!进行重构。

  • 好了,全部完成 → 你有一个好名字了!

  • 函数/变量上方的注释呢?从中删除一切现在在代码中已经包含的内容(名字+参数+类型签名)。如果可以删除整个注释,那就太好了。有时你不能,因为一些东西不能在代码中捕捉到(例如某些假设、解释、示例…),这也没关系。但不要在注释中重复可以在代码中说出的内容。注释是一种必要的恶,它的存在是为了捕捉你的名字和/或类型无法捕捉的知识。

不要过于纠结于一开始总是找到完美的名字 → 多次迭代代码是可以的,每次迭代都会改进您的代码和名字。

以命名为中心的代码审查

一旦你开始深入思考命名,你就会看到它如何改变你的代码审查过程:焦点从查看实现细节转向首先查看名称。

当我进行代码审查时,我会有一个主导性的想法:“这个名字清晰吗?”从那里,整个审查就会发展,并导致整洁的代码。

检查一个名字是解开其背后整个混乱的单一点压力。搜索糟糕的名字,迟早会发现其中有一些糟糕的代码。

进一步阅读

如果您尚未阅读过,请推荐阅读 Robert Martin 的《Clean Code》一书。它有一章关于命名,还深入探讨了如何编写您和其他人都喜欢阅读和维护的代码。