原文:On the Importance of Naming in Programming | Wasp
原标题:On the Importance of Naming in Programming
作者:Martin Sosic
在故事中,你经常会发现主题是有一个强大的恶魔,只有知道它的真名才能控制它。一旦英雄通过狡猾的对话或者调查古老的卷轴找到了那个名字,他们就可以扭转局势并驱逐恶魔!
我坚信编写代码也并没有太大的差别:通过为函数、变量和其他结构找到好的名称,我们能够真正认识到我们正在解决的问题的本质。获得清晰度的结果不仅仅是通过好的名称,还有更干净的代码和改进的架构。
<img src="https://fs.lwmc.net/uploads/2023/11/1699289368991-202311070049530.webp" alt="The power of correct naming in programming" style="zoom: 25%;" />
我敢说,编写干净代码的90%要做的仅仅是正确地命名。听起来很简单,但实际上并非如此!
让我们来看几个例子。
Example #1
代码语言:js复制// 给定一个人的名字和姓氏,返回所有匹配人员的人口统计数据。
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
的名称没有给我们任何关于它包含的内容的信息,这一点因为其元素是通过它们的位置来访问的而变得更糟,这也隐藏了关于它们的任何信息,使我们的代码在它们的排序发生变化时容易默默地出错。
让我们来修复它:
代码语言:js复制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操作(输入/输出,在这里是从数据库获取数据),这对于知道IO相对于纯代码来说比较慢/昂贵很有帮助。 - 我们给其他名称取得也够具有信息量:不多不少刚刚好。
- 请注意我们使用
users
来表示获取到的用户,而不是像usersWithSpecifiedFirstAndLastName
或fetchedUsers
这样更长的名称:不需要使用更长的名称,因为这个变量作用域非常局部、生命周期很短,并且周围上下文足以清楚地说明它的含义。 - 在lambda函数内部,我们使用了单字母名称
u
这可能看起来不太合适。但是,在这里,这是完美的:这个变量的生命周期极短,并且从上下文可以清楚知道它代表什么。而且,我们特意选择字母u
,是因为它是user
的首字母,这样可以让这层关联更加明显。
- 请注意我们使用
- 我们为返回对象中的值命名为
averageAge
和medianSalary
。现在任何使用我们的函数的代码就不需要依赖结果中项目的顺序,同时也更易读、更具信息量。
最后,注意函数前面不再需要注释了。实际上,注释已经不再需要了:从函数名称和参数就可以完全清楚函数的作用!
Example #2
代码语言:js复制// 找到一个空闲的机器并使用它,如果需要的话,创建一个新的机器。
// 然后在该机器上,使用给定的Docker镜像和设置命令设置新的工作进程。
// 最后,在该工作进程上开始执行任务并返回其ID。
async function getJobId (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}
在这个示例中,我们忽略了实现细节,只关注名称和参数的正确性。
这段代码有什么问题?
- 函数名称隐藏了很多关于它所做的事情的细节。它根本没有提到我们必须获取机器或设置工作人员,也没有提到该函数将导致创建一个在后台继续执行的作业。相反,它给人一种我们正在做某种简单操作的感觉,因为使用了动词“get”:我们只是获取一个已经存在的作业的ID。想象一下在代码中看到对这个函数的调用:
getJobId(...)
→ 你不会期望它花费很长时间或做所有它实际上做的事情,这是不好的。
好的,这个问题很容易解决,让我们给它一个更好的名称!
代码语言:js复制async function procureFreeMachineAndSetUpTheDockerWorkerThenStartExecutingTheJob (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}
哎呀,这是一个又长又复杂的名称。但事实上,如果我们不失去关于这个函数做什么以及我们可以从中期望什么有价值的信息,我们无法将其缩短。因此,我们陷入了困境,找不到更好的名称!现在怎么办?
问题是,如果没有干净的代码支持,你就无法给出一个好的名称。因此,一个糟糕的名称不仅仅是一个命名错误,而且通常也是一个指示着问题代码的指标,一个设计失败。代码如此有问题,以至于你甚至不知道该给它起什么名字→没有一个直接的名字可以给它,因为它不是一个直接的代码!
在我们的例子中,问题在于这个函数一次尝试做太多事情。一个长的名称和许多参数是这个问题的指标,尽管在某些情况下这些可能是可以接受的。更强烈的标志是在名称中使用了“和”和“然后”这样的词,以及可以通过前缀(如machine
、worker
)进行分组的参数名称。
这里的解决方案是通过将函数分解为多个较小的函数来清理代码:
代码语言:js复制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,并且吓跑了很多人,因为他们以为这是一个全新的通用编程语言!
如何想出一个好的名字
要给出名字,而是去找到它
最好的建议可能不是给出一个名字,而是去找出一个名字。你不应该创造一个全新的名字,就像你在给宠物或孩子取名一样;你应该寻找你所命名的事物的本质,名字应该基于这个本质自然呈现出来。如果你不喜欢你找到的名字,那意味着你不喜欢你所命名的事物,你应该通过改进你的代码设计来改变这个事物(就像我们在第二个例子中所做的那样)。
<img src="https://fs.lwmc.net/uploads/2023/10/1698157426331-202310242223171.webp" alt="You shouldn't name your variables the same way you name your pets, and vice versa" style="zoom:25%;" />
在命名时需要注意的事情
- 首先,确保这不是一个糟糕的名字:)。记住:不要误导,不要省略,不要假设。
- 让它反映出它所代表的内容。找到它的本质,把它代入在名字中。名字还是难看?改进代码。你还有其他东西可以帮助你→类型签名和注释。但这些是次要的。
- 让它与周围的其他名字和谐共处。它应该与它们有明确的关系 - 在同一个“世界”中。它应该与相似的东西相似,与相反的东西相反。它应该和周围的其他名字一起讲述一个故事。它应该考虑到它所在的环境。
- 长度随范围而定。一般来说,名字的生命周期越短,范围越小,名字就可以/应该越短,反之亦然。这就是为什么在短暂的lambda函数中使用单字母变量是可以接受的。如果不确定,选择较长的名字。
- 坚持在代码库中使用术语。如果你到目前为止使用的是
服务器
这个词,那就没有理由开始使用后端
这个词。同样,如果你使用服务器
作为一个术语,你可能不应该选择前端
:相反,你可能会想使用客户端
,这是一个与服务器
更紧密相关的术语。 - 坚持在代码库中使用的约定。我在我的代码库中经常使用的一些约定的例子:
- 当变量是Bool时,前缀是
is
(例如isAuthEnabled
) - 对于幂等函数,前缀是
ensure
,它们只会在尚未设置的情况下做某事(例如分配资源)(例如ensureServerIsRunning
)。
- 当变量是Bool时,前缀是
每次命名的简单技巧
如果你在命名的时候遇到困难,做以下事情:
- 在函数/变量上方写一个注释,用人话描述它是什么,就像你在向同事描述一样。可能是一句话,也可能是多句话。这就是你的函数/变量所做的,它是什么的本质。
- 现在,你扮演雕刻家的角色,你在你的函数/变量的描述上雕刻和塑形,直到你得到一个名字,通过去掉它的一部分。当你觉得你想象中的雕刻刀再敲一下就会去掉太多的时候,你就停下来。
- 你的名字还是太复杂/令人困惑吗?如果是这样,那就意味着背后的代码太复杂,应该重新组织!去重构它。
- 好了,全部完成→你有了一个好名字!
- 那个在函数/变量上方的注释?从中删除现在已经在代码中(名字 参数 类型签名)捕捉到的所有内容。如果你可以删除整个注释,那就太好了。有时候你不能,因为有些东西不能在代码中捕捉到(例如,某些假设,解释,例子,...),这也是可以的。但是不要在注释中重复你可以在代码中说的内容。注释是一种必要的恶,它在这里是为了捕捉你不能在你的名字和/或类型中捕捉到的知识。
不要过于纠结于一开始就找出完美的名字→可以多次迭代你的代码,每次迭代都会提高你的代码和名字。
以命名为重点的代码审查
一旦你开始深思熟虑地命名,你会看到它是如何改变你的代码审查过程的:焦点从查看实现细节转移到首先查看名称。
当我在进行代码审查时,我会有一个主要的思考:“这个名字清晰吗?”。从那里开始,整个审查演变并结果在清晰的代码。
检查一个名字是一个pressure
点,可以解开其背后的所有混乱点。看到不好的命名,你迟早会发现,并存着糟糕的代码。
进一步阅读
如果你还没有读过的话,我建议你阅读Robert Martin的《Clean Code》这本书。它有一章关于命名的内容,还深入讲解了如何编写让你和其他人都喜欢阅读和维护的代码。
此外,还有一个关于命名困难的流行笑话 - TwoHardThings