通过为函数、变量和其他构造找到好的名称,我们才能真正认识到我们正在解决的问题的本质。清晰度的获得不仅仅是好的名称,还有更清晰的代码和改进的架构。
编写清晰代码,正确命名事物。听起来很简单,但实际上并不是!
例子1
// 给出一个人的名和姓,返回所有匹配人员的人口统计数据。
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
被重复使用,覆盖了作为函数参数的 a,使读者困惑,并在将来修改代码并引用错误变量时更容易出错。 - 返回的对象没有关于其包含内容的任何信息,相反,在以后使用时需要小心其元素的顺序。
- 在调用
users()
函数后返回的字段.info
的名称并没有告诉我们它包含什么信息,这进一步加剧了问题,因为通过它们的位置访问其元素也隐藏了任何关于它们的信息,并且使我们的代码容易在其排序更改时默默地出错。
让我们修复它:
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))
};
}
我们做了什么?
- 1.函数的名称现在准确地反映了它的作用,没有多余的也不缺少。名称中的
fetch
甚至表明它执行了一些IO
操作(输入/输出,在这种情况下从数据库中获取),这是值得知道的,因为与纯代码相比,IO
相对较慢/昂贵。 - 2.我们让其他名称足够信息化:既不过多,也不过少。
- 请注意,我们将获取的用户命名为
users
,而不是类似于usersWithSpecifiedFirstAndLastName
或fetchedUsers
的较长名称:没有必要使用较长的名称,因为这个变量非常局部、短暂,并且周围的上下文足够清晰。 - 在匿名函数 内部,我们选择了一个单字母的名称
u
,这可能看起来是不良做法。但在这里,它是完美的:这个变量的生存期非常短,从上下文中清楚地知道它代表什么。此外,我们选择了字母u
有一个原因,因为它是user
的首字母,因此使得这种联系明显。
- 请注意,我们将获取的用户命名为
- 3.我们为返回的对象中的值命名:
averageAge
和medianSalary
。现在任何使用我们函数的代码都不需要依赖结果中项目的顺序,并且阅读起来也很容易和信息化。 - 4.最后,请注意,函数上面没有注释了。问题是,不再需要注释:从函数名称和参数中都很清楚!
例子2
// 找到一个空闲的机器并使用它,或者根据需要创建一个新的机器。
// 然后在该机器上,使用给定的 Docker 镜像和设置命令设置新的工作者。
// 最后,在该工作者上开始执行一个作业并返回其 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
,但却用于认证!而这两者并不相同:认证是关于确认身份,授权是关于授予权限。 - Wasp-lang:
Wasp
是一个全栈JS web
框架,它使用自定义的配置语言只是其代码库的一小部分,但在名字中加入了-lang
,并吓跑了很多人,因为他们以为它是一个全新的通用编程语言!
如何想出一个好名字
最好的建议不是给一个名字,而是找到一个名字。你不应该像给宠物或孩子取名字那样,编造一个原始的名字;相反,你应该找出你所命名的事物的本质,基于它来给出名字。如果你不喜欢你发现的名字,那意味着你不喜欢你所命名的事物,你应该通过改进代码的设计来改变这个事物(就像我们在示例 #2 中所做的那样)。
你不应该像给你的宠物命名一样给你的变量命名,反之亦然
找到一个名字时要注意的事项
- 首先,确保它不是一个糟糕的名字. 记住:不要误导,不要省略,不要假设。
- 使它反映它所代表的东西。找到它的本质,在名字中表达出来。名字仍然难看?改进代码。你还有其他东西可以帮助你 → 类型签名和注释。但这些是次要的。
- 使它与周围的其他名称和谐相处。它应该与它们有明显的关系 - 在同一个“世界”中。它应该与类似的东西相似,与相反的东西相反。它应该与周围的其他名称一起讲述一个故事。它应该考虑到它所处的上下文。
长度遵循范围。一般来说,名称的生存期越短,范围越小,名称就越短,反之亦然。这就是为什么在短函数中- 使用单字母变量可能没问题的原因。如果不确定,就选择更长的名字。
坚持在代码库中使用的术语。如果到目前为止你使用了术语server
,就不要毫无理由地开始使用backend
这个术语。同样,如果你使用server
作为术语,你可能不应该使用frontend
:相反,你可能想要使用client
,这是一个与server
更密切相关的术语。 - 坚持使用代码库中的约定。一些我经常在我的代码库中使用的约定的例子:
- 当变量是
Bool
时使用前缀(例如isAuthEnabled
) - 对于具有幂等性的函数使用前缀
ensure
,这些函数只会在到目前为止尚未设置的情况下执行某些操作(例如分配资源)(例如ensureServerIsRunning
)。
- 当变量是
每次想出名字时的简单技巧
如果你在想出一个名字时遇到困难,可以按照以下步骤操作:
- 在函数/变量上方写一个注释,描述它是什么,用人类语言描述,就像你在向同事描述一样。可能是一句话,也可能是多句话。这是你的函数/变量所做的事情的本质,它是什么。
- 现在,你扮演雕塑家的角色,你雕刻和塑造函数/变量的描述,直到你得到一个名字,通过拿走其中的一部分。当你感觉到再敲一下你想象中的凿子会太过分时,就停止。
- 你的名字还是太复杂/令人困惑吗?如果是这样,那意味着背后的代码太复杂了,应该重新组织!去重构它。
好了,所有的都做完了 → 你有一个好名字! - 函数/变量上方的注释?从中删除现在在代码中已经包含的所有内容(名称 + 参数 + 类型签名)。如果你可以删除整个注释,那太好了。有时你不能这样做,因为有些东西无法在代码中捕获(例如某些假设、解释、示例等),这也没关系。但不要在注释中重复你可以在代码中说的话。注释是一种必要的恶,它是用来捕捉你不能在你的名称和/或类型中捕捉到的知识的。
不要过于纠结于一开始就想出完美的名字 → 进行多次代码迭代是可以的,每次迭代都会随着你的代码和名字的改进而改进。
以命名为重点审查代码
一旦你开始思考命名,你就会看到它如何改变你的代码审查过程:重点从查看实现细节转移到首先查看名称。
当我进行代码审查时,我会有一个主要的想法:“这个名字清楚吗?”从这里开始,整个审查过程会演变成清洁的代码。
检查一个名称是解开其背后整个混乱的一个单一压力点。搜索糟糕的名称,你迟早会发现糟糕的代码,如果有的话。
命名通用规范
-
1.为了能让命名更加易懂和易读,尽量不要缩写/简写单词,除非这些单词已经被公认可以被这样缩写/简写。比如 CustomThreadFactory 不可以被写成 ~~CustomTF 。
-
2.命名不像函数一样要尽量追求短,可读性强的名字优先于简短的名字,虽然可读性强的名字会比较长一点。 这个对应我们上面说的第 1 点。
-
3.避免无意义的命名,你起的每一个名字都要能表明意思。
- 正例:UserService userService; int userCount;
- 反例: UserService service int count
-
4.避免命名过长(50 个字符以内最好),过长的命名难以阅读并且丑陋。
-
5.不要使用拼音,更不要使用中文。 注意:像 alibaba 、wuhan、taobao 这种国际通用名词可以当做英文来看待。
- 正例:discount
- 反例:dazhe
Codelf:变量命名神器?
这是一个由国人开发的网站,网上有很多人称其为变量命名神器, Guide 在实际使用了几天之后感觉没那么好用。小伙伴们可以自行体验一下,然后再给出自己的判断。
我选择了 Java 编程语言,然后搜索了“序列化”这个关键词,然后它就返回了很多关于序列化的命名。
并且,Codelf 还提供了 VS code 插件,看这个评价,看来大家还是很喜欢这款命名工具的。
网友评论