编程中命名的重要性


为函数、变量和其他结构找到好的名称,我们真正认识到我们正在解决的问题的本质。

清晰性的结果不仅是好的名称,还有更清晰的代码和改进的体系结构。

90% 的干净代码编写“只是”正确命名。

例子#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 在 map 内部的 lambda 中重复使用,给作为函数参数的 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))
  };
}

我们做了什么?

  • 函数名中的 fetch 甚至表示它做了一些 IO(输入/输出,在本例中是从数据库中获取)工作,知道这一点很有好处,因为与纯代码相比,IO 的速度相对较慢/成本较高。
  • 我们让其他名称具有足够的信息量:不能太多,也不能太少。
  • 请注意我们是如何为获取的用户使用 users 这个名称的,而不是使用 usersWithSpecifiedFirstAndLastName 或 fetchedUsers 这样更长的名称:没有必要使用更长的名称,因为这个变量是非常局部的、短暂的,而且它周围的上下文足以让人清楚它是关于什么的。
  • 在 lambda 中,我们使用了一个单字母的名称 u,这看起来可能是一种不好的做法。但在这里,它却是完美的:这个变量的寿命极短,而且从上下文中可以清楚地看出它代表什么。此外,我们特别选择 u 这个字母也是有原因的,因为它是 user 的第一个字母,因此这种联系是显而易见的。
  • 我们为返回对象中的值命名为:平均年龄(averageAge)和工资中位数(medianSalary)。现在,任何使用我们函数的代码都不需要依赖结果中项的排序了,而且阅读起来也会更轻松、更翔实。

最后,请注意函数上方已经没有注释了。事实上,注释已不再需要:从函数名和参数中可以清楚地看出这一点!

例子#2

// 找到一台空闲的机器并使用它,或根据需要创建一台新机器
// 如果需要的话。然后在该机器上,使用给定的 Docker 映像和设置 cmd
// 使用给定的 Docker 映像和设置 cmd。最后
// 开始在该 Worker 上执行作业并返回其 id。
async function getJobId (
  machineType, machineRegion,
  workerDockerImage, workerSetupCmd,
  jobDescription
) {
  ...
}

在本例中,我们将忽略执行细节,只关注如何正确使用名称和参数。

这段代码有什么问题?

函数名称隐藏了很多执行细节。
它完全没有提到我们必须采购机器或设置 Worker,也没有提到该函数将创建一个作业,该作业将在后台某个地方继续执行。相反,由于使用了 get 这个动词,它给人的感觉就像是在做一件简单的事情:我们只是在获取一个已经存在的作业的 id。想象一下,在代码中的某个地方看到对该函数的调用:getJobId(...) → 你不会想到它会花很长时间,也不会想到它会做它真正要做的所有事情,这就不好了。

好吧,这听起来很容易解决,我们给它起个好听点的名字吧!

async function procureFreeMachineAndSetUpTheDockerWorkerThenStartExecutingTheJob (
  machineType, machineRegion,
  workerDockerImage, workerSetupCmd,
  jobDescription
) {
  ...
}

呃,这个名字真是又长又复杂。但事实是,我们真的无法在不丢失关于这个函数的作用和我们对它的期望的宝贵信息的情况下,让它变得更短。因此,我们被卡住了,找不到更好的名字!现在怎么办?

问题是,如果名字背后没有简洁的代码,就无法起一个好名字。因此,一个糟糕的名字不仅仅是命名上的失误,往往还表明背后的代码有问题,是设计上的失败。代码有问题,以至于你甚至不知道该给它取什么名字:没有一个直截了当的名字可以给它,因为它不是一个直截了当的代码!

在我们的例子中,问题在于这个函数试图同时做太多事情。冗长的名称和众多的参数都是这方面的指标,不过在某些情况下也没有问题。更强的指标是名称中使用 "和 "和 "然后",以及可以通过前缀(机器、工人)分组的参数名称。

解决方法是将函数分解为多个较小的函数,从而清理代码:

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

什么是好名字?
但是,让我们退一步说--什么是坏名字,什么是好名字?这意味着什么,我们如何识别它们?

好名字不会误导、不会遗漏、不会假设。

一个好的名称应该能让你很好地了解变量的内容或函数的作用。一个好的名称会告诉你所要知道的一切,或者会告诉你足够的信息,让你知道下一步该去哪里找。它不会让你猜测或疑惑。它不会误导你。一个好的名称是显而易见的,也是众望所归的。前后一致。没有过多的创意。不会假定读者不可能了解的背景或知识。

上下文 才是王道
如果没有阅读上下文,就无法对名称进行评估。这取决于故事、环境和代码要解决的问题。名称会讲述一个故事,它们需要像故事一样结合在一起。

如何起一个好名字?
不要给名字,要找到它

最好的建议也许不是起一个名字,而是找出一个名字。你不应该像给宠物或孩子取名字一样,编造一个新颖的名字;你应该寻找你要命名的事物的本质,并在此基础上取名字。如果你不喜欢你发现的名字,那就意味着你不喜欢你要命名的东西,你应该通过改进代码的设计来改变它(就像我们在 #2 例子中所做的)。

取名时应注意的事项

  • 首先,确保不是一个糟糕的名字:)。记住:不要误导、不要遗漏、不要假设。
  • 让它反映出它所代表的东西。找到它的精髓,在名字中抓住它。名称仍然难看?改进代码。你还有其他东西可以帮你:类型签名和注释。但这些都是次要的。
  • 让它与周围的其他名称配合得更好。它应该与其他名称有明确的关系--在同一个 "世界 "中。它应该与相似的东西相似,与相反的东西相反。它应与周围的其他名称共同构成一个故事。应考虑到所处的环境。
  • 长度随范围而定。一般来说,名称的寿命越短,范围越小,名称就可以/应该越短,反之亦然。这就是为什么在短 lambda 函数中可以使用单字母变量的原因。如果不确定,可以使用较长的名称。
  • 坚持使用代码库中的术语。如果你一直使用服务器,那么不要无缘无故地开始使用后台。此外,如果您使用服务器作为术语,那么很可能就不应该使用前端:相反,您很可能希望使用客户端,因为客户端与服务器的关系更为密切。
  • 坚持你在代码库中使用的约定。举例说明我在代码库中经常使用的一些约定:
    前缀为 Bool 的变量(如 isAuthEnabled)
    前缀 ensure 用于idempotent 函数,该函数只有在尚未设置时才会执行某些操作(如分配资源)(如 ensureServerIsRunning)。

每次都能想出名字的简单技巧
如果你在想名字时遇到困难,请按以下步骤操作:

  • 在函数/变量上方写一个注释,用人类语言描述它是什么,就像你在向同事描述一样。可以是一句话,也可以是多句话。这就是你的函数/变量的本质,也就是它是什么。
  • 现在,你要扮演雕刻家的角色,对你的功能/变量的描述进行雕琢和塑造,直到你得到一个名字,你要把它的碎片去掉。当你觉得你想象中的凿子再凿下去会凿去太多东西时,你就会停下来。
  • 你的名字是否仍然过于复杂/令人困惑?如果是这样,那就意味着后面的代码太复杂了,应该重新组织!去重构它吧。
  • 好了,一切就绪:你有了一个好名字!
  • 函数/变量上方的注释?删除代码中的所有内容(名称 + 参数 + 类型签名)。如果能删除整个注释,那就太好了。有时不能,因为有些东西无法在代码中体现(例如某些假设、解释、示例......),这也没关系。但不要在注释中重复你可以在代码中表达的内容。注释是必要之恶,是为了捕捉您无法在名称和/或类型中捕捉到的知识。

在审查代码时考虑命名
一旦你开始大量思考命名问题,你就会发现它将如何改变你的代码审查过程:重点从查看实现细节转移到首先查看名称。

当我进行代码审查时,我主要会考虑一个问题:"这个名称清楚吗?从那时起,整个审查过程都会发生变化,并最终产生整洁的代码。

检查名称是一个单一的压力点,它能解开名称背后的一团乱麻。搜索糟糕的名称,迟早会发现糟糕的代码(如果有的话)。

如果您还没有读过这本书,我建议您读一读罗伯特-马丁(Robert Martin)写的《清洁代码》(Clean Code)一书。书中关于命名的章节非常精彩,而且还进一步介绍了如何编写自己和他人都喜欢阅读和维护的代码。