为什么命名是编写干净代码的首要技能
示例 #1
示例 2
什么是好名字?
臭名昭著的名字的例子
支持我们!🙏⭐️
如何想出一个好名字
审查代码时要考虑命名
进一步阅读
故事中经常会出现这样一种主题:一个强大的恶魔,只有知道它的真名才能被控制。一旦英雄找到这个名字,通过巧妙的对话或查阅古籍,他们就能扭转局势,驱逐恶魔!
我坚信编写代码其实并没有什么不同:通过为函数、变量和其他结构找到合适的名称,我们才能真正理解所解决问题的本质。清晰的思路不仅会带来好的名称,还能带来更清晰的代码和更完善的架构。
我甚至可以说,编写干净代码的90%“仅仅”在于正确命名。
听起来很简单,但事实并非如此!
让我们看几个例子。
示例 #1
// 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
信息。 a
在 lambda 内部被重用map
,遮蔽了函数参数a
,使读者感到困惑,并且使得在将来修改代码时更容易犯错误并引用错误的变量。- 返回的对象没有任何关于其包含什么的信息,相反,您在稍后使用它时需要注意其元素的顺序。
.info
函数调用结果中的字段名称没有users()
给我们提供任何有关其包含内容的信息,而通过其位置访问其元素则使情况变得更糟,同时还隐藏了有关它们的任何信息,并且如果它们的顺序发生变化,我们的代码就容易出现错误。
让我们修复它:
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
。现在,任何使用我们函数的代码都不需要依赖于结果中项目的顺序,而且阅读起来也更简单、信息量更大。
最后,注意到函数上方不再有注释了。事实上,注释已经不再需要了:从函数名和参数就可以看出来了!
示例 2
// 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(...)
→你并没有预料到它会花费很长时间或完成它实际执行的所有任务,这很糟糕。
好的,这听起来很容易修复,让我们给它一个更好的名字!
async function procureFreeMachineAndSetUpTheDockerWorkerThenStartExecutingTheJob (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}
哎呀,这名字又长又复杂。但事实上,我们无法缩短它,否则会丢失关于这个函数功能以及我们能从中得到什么的宝贵信息。因此,我们陷入了困境,找不到更好的名字!现在怎么办?
问题是,如果没有清晰的代码,就无法给出一个好的名字。所以,糟糕的名称不仅仅是命名失误,通常还表明其背后的代码存在问题,设计失败。代码问题太多,你甚至不知道该如何命名 → 没有一个直接的名称可以给它命名,因为它本身就不简单!
在我们的例子中,问题在于这个函数试图一次性执行太多操作。较长的名称和大量参数是这种情况的体现,尽管在某些情况下这些是可以接受的。更明显的体现是名称中使用“and”和“then”等词,以及参数名称可以通过前缀 ( machine
, worker
) 进行分组。
这里的解决方案是通过将函数分解为多个较小的函数来清理代码:
async function procureFreeMachine (type, region) { ... }
async function setUpDockerWorker (machineId, dockerImage, setupCmd) { ... }
async function startExecutingJob (workerId, jobDescription) { ... }
什么是好名字?
但让我们退一步思考——什么是坏名字,什么是好名字?它们意味着什么?我们该如何识别它们?
好名字不误导、不遗漏、不假设。
一个好的名字应该让你清楚地了解变量包含的内容或函数的功能。一个好的名字会告诉你所有需要了解的信息,或者足以让你知道下一步该怎么做。它不会让你猜测或疑惑,也不会误导你。一个好的名字显而易见,并且符合预期。它保持一致,不会过于夸张。它不会假设读者不太可能了解的背景或知识。
另外,上下文至关重要:你无法在不考虑上下文的情况下评价一个名字。它verifyOrganizationChainCredentials
可能是一个糟糕的名字,也可能是一个很棒的名字。它a
可能是一个很棒的名字,也可能是一个糟糕的名字。这取决于故事本身、环境以及代码要解决的问题。名字本身就讲述了一个故事,它们需要像故事一样紧密地联系在一起。
臭名昭著的名字的例子
- JavaScript
- 我自己就是这个错误名字的受害者:当我想学习 Java 时,我的父母给我买了一本关于 JavaScript 的书。
- HTTP 授权标头
- 它名为
Authorization
,但却用于身份验证!两者并不相同:身份验证是用于识别身份,而授权是用于授予权限。更多信息请参见:https ://stackoverflow.com/questions/30062024/why-is-the-http-header-for-authentication-called-authorization 。
- 它名为
- 黄蜂语言:
- 这是我的错:Wasp是一个全栈 JS Web 框架,它使用自定义配置语言作为其代码库的一小部分,但我加上了
-lang
这个名字,吓跑了很多人,因为他们以为这是一种全新的通用编程语言!
- 这是我的错:Wasp是一个全栈 JS Web 框架,它使用自定义配置语言作为其代码库的一小部分,但我加上了
支持我们!🙏⭐️
为了帮助我们提升 Wasp-lang 的声誉😁,不妨在 Github 上给我们点个星!Wasp 所做的一切都是开源的,您的支持有助于我们简化 Web 开发,并激励我们撰写更多类似的文章。
如何想出一个好名字
不要给出名字,找到它
最好的建议或许不是直接给它起名字,而是去发现它。你不应该像给宠物或孩子起名字那样,自己编造一个新名字;你应该寻找你所命名事物的本质,并基于此来赋予它新的意义。如果你不喜欢你发现的名字,那就意味着你不喜欢你命名的事物,你应该通过改进代码设计来改变它(就像我们在示例 #2 中所做的那样)。
起名字时要注意的事项
- 首先,确保它不是一个坏名字:)。记住:不要误导,不要省略,不要假设。
- 让它反映它所代表的含义。找到它的本质,并在名称中体现它。名称仍然不好看?那就改进代码吧。你还有其他东西可以帮到你→类型签名和注释。但这些是次要的。
- 让它与周围的其他名称协调一致。它应该与它们有清晰的联系——处于同一个“世界”。它应该与相似的东西相似,与相反的东西相反。它应该与周围的其他名称共同构成一个故事。它应该考虑到它所处的语境。
- 长度取决于作用域。通常,名称的生命周期越短,作用域越小,名称就可以/应该越短,反之亦然。这就是为什么在短的 lambda 函数中使用单字母变量是可以的。如果不确定,可以选择较长的名称。
- 请严格遵循代码库中使用的术语。如果您目前使用的是术语
server
,请不要无缘无故backend
地改用 。此外,如果您使用server
作为术语 ,则可能不应该使用frontend
: ,而应该使用client
,因为它与 更接近server
。 - 遵循代码库中使用的约定。以下是我在代码库中经常使用的一些约定示例:
is
当变量为 Bool 时添加前缀(例如isAuthEnabled
)- 幂等函数的前缀
ensure
,仅当尚未设置时才会执行某些操作(例如分配资源)(例如ensureServerIsRunning
)。
每次都能猜出名字的简单技巧
如果您在想名字时遇到困难,请执行以下操作:
- 在函数/变量上方写一段注释,用人类语言描述它的含义,就像你向同事描述它一样。注释可以是一句话,也可以是多句话。注释的核心内容就是你的函数/变量的作用,以及它的含义。
- 现在,你扮演雕塑家的角色,通过凿刻和塑造函数/变量的描述,去掉它的一部分,直到你得到一个名字。当你觉得再用想象中的凿子敲击一遍就会去掉太多东西时,你就停下来。
- 你的名字还是太复杂/令人困惑吗?如果是这样,那就意味着背后的代码太复杂了,应该重新组织!去重构吧。
- 好的,全部完成→你的名字好听!
- 函数/变量上方的注释?删除所有现在包含在名称 + 参数 + 类型签名中的内容。如果可以删除整个注释,那就太好了。有时你不能删除,因为有些内容无法在名称中捕获(例如某些假设、解释、示例……),这也可以。但不要在注释中重复你可以在名称中表达的内容。注释是必要之恶,它的作用是捕获你无法在名称和/或类型中捕获的知识。
不要总是在一开始就纠结于找出完美的名字→可以对代码进行多次迭代,每次迭代时代码和名称都会得到改进。
审查代码时要考虑命名
一旦您开始认真思考命名,您就会看到它将如何改变您的代码审查流程:重点从查看实现细节转移到首先查看名称。
当我进行代码审查时,我主要会思考一个问题:“这个名字清楚吗?”由此开始,整个审查逐渐展开,最终得到干净的代码。
检查名称就像施加压力,可以解开其背后的所有混乱。搜索不良名称,迟早会发现不良代码(如果有的话)。
进一步阅读
如果你还没读过,我推荐你读一读Robert Martin 的《代码整洁之道》 。书中有一章讲解了命名,非常精彩,并且还深入讲解了如何编写你和其他人都乐于阅读和维护的代码。
此外,还有一个关于命名困难的流行笑话。
文章来源:https://dev.to/wasp/why-naming-is-1-skill-for-writing-clean-code-4a5p