写出好维护的程序代码 — 写程序时如何做好命名 (naming)?

2025年9月13日

💎 加入 E+ 成長計畫 與超過 750+ 位工程師一同在社群成長,並獲得更多深度的軟體前後端學習資源

先前斯坦福大学计算机科学系教授 John Ousterhout 在他的经典著作《A Philosophy of Software Design》一书中分享过他亲身经历过的惨痛教训。他提到当年在开发一个分布式操作系统遇到一个离奇的 bug,在某些时候,文件会遇到数据丢失的状况,某一个区块的数据,即使没有被用户改动,也会突然全部归零。

这个 bug 花了 John Ousterhout 教授与他的研究生团队,整整半年的时间,才总算找出问题所在。而这个问题所在,就是因为命名不清楚,导致变量被误用。但也因为命名不清楚,所以被误用后,不容易被察觉,导致要找出 bug 所在,变得特别花时间。

具体来讲,当时在操作系统中的文件系统会用到 block 这个变量,但事实上这个 block 是有两个不同的作用,一个是物理磁盘上的区块编号,另一个是作为文件中的逻辑区块编号;所以当两种混用在一起,就导致某个应该不该被碰到区块,被覆写归零。

在看完上面这个问题,读者们会觉得,假如要用比较好的命名方式,要如何重构这两个变量名称呢?

John Ousterhout 教授在重构时,选择用 fileBlockdiskBlock 来重新命名。通过这个重构,这两个变量名称就变得非常清晰,该用来处理文件中逻辑的,就用 fileBlock;如果是处理物理磁盘的,则用 diskBlcok,两者不会有搞混的时候。

希望通过上面的案例,有让读者们感受到,假如没有做好命名,很可能会要面对这种意料之外的 debug 成本。假如想要避免的话,掌握一些命名的原则,会很有帮助。以下汇整了一些推荐读者们在命名时,可以用上的原则。

推荐的命名原则:精确 (precise)

首先,命名最重要的原则之一就是要精确。精确的意思是不能模糊,要让人能够一眼看懂。

一个有效让命名精确的方式,是问自己「如果某个人看到这个名称,假如不看其他细节,例如不看文档、不看变量或函数内容,或者不看这个变量或函数用在哪,那个人有办法推测出这个变量或函数在做什么吗?」

针对上面这个问题,如果没有办法,那就很可能代表函数不够精确。

会不精确,通常是因为某个名称试图涵盖太多东西,就以上面 John Ousterhout 教授分享的例子来说,block 这个变量涵盖的面太广,所以可能被有不同的诠释,而当有了诠释的空间,就可能让这个变量被误用。

除了上面的 block 案例,让我们多看几个例子,来具体了解如何把不精确的命名,调整成足够精确的。

首先,很多人可能会觉得用 result 来表达回传的结果,或者用 count 来表达某个正在被计算的数,这样的命名就足够精确。但是通常这样的命名,仍是不够精确地,因此可能导致像上面提到的问题。要调整的话,可以问「result 是什么的结果? count 是在算什么的?」,然后把这些信息融入到命名中,就会更精确。

除此之外,不同性质的变量名,也要尽可能做到让人一眼能看出其性质。举例来说,const error = true; 或者 const status = false 都是不理想的布尔值命名,因为这很难让人第一眼就看出这两个变量是布尔值。

假如用 error 很可能让人诠释成不同的错误类别,而假如是 status 则可能会让人诠释成某个联合类型,例如很常见的 status 会有 "pending" | "completed" | "failed" 这样的表达。

但其实可以很简单通过 ishas 这类字首,让人一眼看出该变量是布尔值。例如 hasRegistrationError 或者 isAccountActive。当改成这种方式命名,就能够更精确,也更容易让读到这个命名的其他维护者,能不看细节内容,也知道这是布尔值。

精确这个原则,不只在一般的变量,在函数与方法的命名也同样适用。具体来说,需要确保该函数或方法在做的事情,能够精确地反映到命名上。

有些书籍会说,函数的命名要用动词加受词,来表达对某件事情做某个操作。然而,如果只是这样命名函数,仍然可能会不够精确。举例来说,下面这个例子的 handleUsers 函数,就相对不是太好的命名方式。因为假如只看名称的话,不知道 handle 是要处理什么。

function handleUsers(users: User[]): User[] {
  return users.filter((user) => user.isActive);
}

但是假如看该函数的内容,显然是在把 isActive 的使用者挑出来,所以比较好的命名会是改成 filterActiveUsers。因此,比起公式化地用动词加受词,更关键的还是回到该函数的本质,然后检视命名是否反映其本质。

要多精确? 看涉及的范围

上一段我们谈到,在命名上要尽可能精确。然而,可能会有读者问,命名到底要到多精确才行? 假如在一个 for 循环中用 ij 恰当吗?

关于这个问题,在目前社群中普遍推荐的判断原则,是看要命名的东西影响的范围有多大。以上面谈到的 block 来说,假如是在整个系统中,许多地方都会用到,意味着影响范围很大,这时候就要越精确越好。换句话说,一个变量 (或函数) 的「声明位置」与「使用位置」之间的程序代码距离越远,它的名称就应该越长、越具描述性。

反之,假如某一个 for 循环中的索引,只会被用在该 for 循环当中,不会被用在其他地方,这种时候命名简短一点其实不会有太大的问题。所以假如用 for (let i = 0, i < array.length; i++) 这种写法,多数时候是没问题的。

如果觉得难精确命名,代表程序可能该重构

读完前一个段落,相信有些读者可能会问「假如没办法决定一个精确的命名该怎么办? 」

假如有这种问题,往往意味着程序代码可能写得有问题、需要重构。因为通常会觉得无法精确命名,是因为可能某个函数或方法,太包山包海了,所以导致该函数或方法,没有办法被单一个名称定义出来。

先前我们在 写出好维护的程序代码 — 高内聚 一文就有谈到,为函数或方法设定边界,确保在该函数与方法内的所有内容相关性都是高的,不仅会让程序好维护,也能减少「因为函数做太多事,所以很难精确命名」的状况。

前面谈到的 John Ousterhout 教授就提过「如果你很难为一个变量或方法找到一个简单的名称,来清楚描绘它背后的实体 (对象或概念) ,这通常是一个警讯,代表那个实体本身的设计可能不够简洁。」

If it's hard to find a simple name for a variable or method that creates a clear image of the underlying object, that's a hint that the underlying object may not have a clean design. — John Ousterhout

阅读更多

关于命名,除了精确外,还有其他要关注的重点,我们在 E+ 成长计划的主题文有更深入的讨论,感兴趣的读者,欢迎加入 E+ 阅读 (链接)。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們