五、给事物命名很难
名字到处都是。 它们是我们的思维抽象化宇宙复杂性的方式。 在软件世界中,我们总是忙于制作新的抽象概念来描述我们的日常现实。 在编程世界中,一个常见的妙语是:给事物命名很难。 想出一个名字并不总是很难,但想出一个好的名字通常很难。
在前几章中,我们探讨了抽象的基本原理和理论。 在本章中,我们将提供解开这个谜题的最终答案。 没有好的命名,抽象就不能成为好的抽象。 在我们使用的名称中,我们提炼了一个概念,而这个提炼将决定人们最终如何理解这个概念。 所以,命名事物不仅仅是提供任意的标签; 它是理解的规定。 只有通过一个好的名称,用户或其他程序员才能完全内化我们的抽象,并充分理解它。
在本章中,我们将使用一些例子来探索使一个好名字的关键特征。 我们还将讨论在动态类型语言(如 JavaScript)中命名事物所面临的挑战。 从这一章开始,我们应该清楚地理解产生清晰的描述性名称所涉及的内容。
具体来说,我们将涵盖以下主题:
- 名字有什么关系?
- 命名的反模式
- 一致性和层次结构
- s
名字有什么关系?
打破一个好名字的关键要素是困难的。 它似乎更像是一门艺术而不是科学。 相当好的名字和非常好的名字之间的界限是模糊的,容易受到主观意见的影响。
考虑一个负责将多个 CSS 样式应用到按钮的函数。 设想一个场景,其中这是一个独立的函数。 你认为以下哪个名字最合适?
styleButton
setStyleOfButton
setButtonCSS
stylizeButton
setButtonStyles
applyButtonCSS
你可能已经选了你最喜欢的。 读这本书的人,肯定会有不同意见。 这些分歧中有许多是基于我们自己的偏见。 我们的许多偏见会受到一些因素的制约,比如我们说什么语言,我们以前接触过什么编程语言,我们花时间创建什么类型的程序。 我们每个人之间都存在着许多差异,然而,不知何故,我们必须想出一个非模糊的概念,一个好的或干净的名字是什么。 至少,我们可以说一个好的名字可能具有以下特征:
- Purpose 目的
- 概念:其核心思想及思考方式
- 合同:关于它如何工作的期望
这并没有完全涵盖命名的复杂性,但有了这三个特征,我们就有了一个起点。 在本节的其余部分中,我们将学习这些特征对命名过程的重要性。
目的
一个好的名字表明目的。 目的是指某物做了什么,或者某物是什么。 对于函数来说,它的目的就是它的行为。 这就是为什么函数通常以动词形式命名,如getUser
或createAccount
,而存储值的东西通常是名词,如account或button。
一个包含明确目的的名称永远不需要进一步解释。 这应该是不言而喻的。 如果一个名称需要注释来解释它的用途,那么这通常表明它还没有完成作为名称的工作。
某事物的目的是高度相关的,因此将由周围的代码和该名称所在的代码库区域来决定。 这就是为什么使用通用名称通常是可以的,只要它周围有上下文,有助于告知它的目的。 例如,比较TenancyAgreement
类中的这三个方法签名:
class TenancyAgreement {
// Option #1:
saveSignedDocument(
id,
timestamp
) {}
// Option #2:
saveSignedDocument(
documentId,
documentTimestamp
) {}
// Option #3:
saveSignedDocument(
tenancyAgreementSignedDocumentID,
tenancyAgreementSignedDocumentTimestamp
) {}
}
当然,这是有主观性的,但大多数人会同意,当我们有一个周围的环境,它的目的很好地传达了,我们不应该需要粒度化该环境中的每个变量的命名。 考虑到这一点,我们可以说,在前面的代码中Option #1
太有限,可能会引起歧义,Option #3
是不必要的冗长,因为它的参数名的一部分已经由其上下文提供了。 然而,Option #2
与documentId
和documentTimestamp
的搭配恰到好处:它充分传达了论点的目的。 这就是我们所需要的。
目的是任何名字的核心。 没有描述或目的的指示,名称只是一种装饰,通常意味着我们的代码的用户只能在文档和其他代码段之间翻找,只是为了弄清楚一些东西。 因此,我们必须时刻考虑我们的名字是否能很好地传达目的。
概念
一个好的名字表明概念。 一个名字的概念指的是它背后的想法,创造它的意图,以及我们应该如何思考它。 例如,一个名为relocateDeviceAccurately
的函数不仅告诉我们它将做什么(它的目的),还告诉我们关于它行为的概念。 从这个名字,我们可以看到设备是可以定位的东西,定位这样的设备可以在不同的精度水平上完成。 一个相对简单的名字可以在读者的脑海中唤起一个丰富的概念。 这是命名事物的重要力量之一:名称是理解事物的途径。
一个名字的概念,就像它的目的一样,与它所处的环境紧密相关。 上下文是存在我们名字的共享空间。 我们感兴趣的名字周围的其他名字绝对有助于我们理解它的概念。 把下面的名字想象在一起:
rejectedDeal
acceptedDeal
pendingDeal
stalledDeal
通过这些名称,我们立即明白,一项交易至少可以有四个不同的状态。 这意味着这些国家是相互排斥的,不能同时适用于一项协议,尽管目前尚不清楚。 我们可能会假设,与交易是否悬而未决或停滞有关的具体条件是什么,尽管我们不确定这些条件是什么。 所以,即使这里有歧义,我们已经开始建立对问题领域的丰富理解。 这只是通过查看名称——甚至不需要阅读实现。
我们说过语境是一种名字的共享空间。 在编程术语中,我们通常说,在一个区域中一起命名的事物占据一个名称空间。 名称空间可以被认为是事物彼此共享概念区域的地方。 有些语言已经将名称空间的概念形式化为其自己的语言构造(通常称为包,或简称为名称空间)。 即使没有这种正式的语言结构,JavaScript 仍然可以通过层次化结构来构造名称空间,比如像这样的对象:
const app = {};
app.transactions = {};
app.transactions.dealMaking = {};
app.transactions.dealMaking.states = [
'REJECTED_DEAL',
'ACCEPTED_DEAL',
'PENDING_DEAL',
'STALLED_DEAL'
];
大多数程序员倾向于认为名称空间是一种非常正式的结构,但情况并非如此。 通常,在不知情的情况下,当我们在函数中编写函数时,就构成了隐含的名称空间。 在这种情况下,名称空间不是由对象层次结构的层次描述的,而是由函数的作用域描述的,如下所示:
function makeFilteredRequest(endpoint, filterFn) {
return fetch(`/${endpoint}/`)
.then(response => response.json())
.then(data => data.filter(filterFn);
}
在这里,我们通过fetch
向端点发出请求,在返回之前,我们通过利用fetch
返回的承诺收集所需的数据。 为此,我们使用两个then(...)
处理程序。
A promise is a natively provided class that provides a useful abstraction for handling asynchronous actions. You can usually identify a promise by its then method, like what we used in the preceding code. It's common practice to either use promises or callbacks when tapping into asynchronous actions. You can read more about this in Chapter 10, Control Flow, in the Asynchronous control flow section.
我们的第一个then(...)
处理器将其参数命名为response,而第二个处理器将其参数命名为data
。 在makeFilteredRequest
的语境之外,这些术语将非常模糊。 但是,因为我们是在与发出过滤请求相关的函数的隐含名称空间中,所以术语响应和数据足以表达它们的概念。
通过名称传达的概念,就像它们的目的一样,与它们所指定的上下文紧密交织在一起,所以不仅要考虑名称本身,还要考虑它周围的一切:名称所处的复杂的逻辑和行为。 所有代码都处理某种程度的复杂性,对这种复杂性的概念理解对于驾驭它至关重要。 所以,在命名某样东西时,最好问问自己:我希望它们如何理解这种复杂性? 这是相关的,如果你正在制作一个简单的界面供其他程序员使用,编写一个深嵌入的硬件驱动程序,或创建一个 GUI 供非程序员使用。
合同
一个好的名称表示与周围抽象的其他部分的合同。 一个变量,通过它的名字,可以指示它将如何被使用,它包含什么类型的值,以及我们应该对它的行为有什么一般的期望。 这通常不会被想到,但当我们命名某样东西时,实际上,我们是在建立一系列隐含的期望或,这些期望或将定义人们如何理解和使用该东西。 下面是一些 JavaScript 中存在的隐藏契约的例子:**
* 以为前缀的变量为,如isUser
,则为布尔类型(true
或false
)。
* 全大写的变量应该是常量(只设置一次且不可变),例如,DEFAULT_USER_EXPIRY
。
* 以复数形式命名的变量(例如,elements)应该包含一个或多个类集对象(例如,数组)中的项,而以单数形式命名的变量(例如,element)只应该包含一个项(不在集合中)。
* 名字以get
、find
或select
开头的函数通常会返回一些东西给你。 以process
、build
或run
开头的函数比较模糊,可能不会这样做。
* 以下划线开头的属性或方法名,例如_processConfig
,通常用于内部实现或伪私有。 它们不打算被公开调用。
不管我们喜不喜欢,所有的名字都带有一种包袱,那就是对他们的价值观和行为有着不可避免的期望。 了解这些约定是很重要的,这样我们就不会意外地破坏其他程序员所依赖的契约。 当然,每个公约在不适用的情况下都会有例外,但无论如何,我们应该尽可能地遵守它们。
不幸的是,没有一个规范的列表来定义所有这些契约。 它们通常是非常主观的,并且取决于代码库。 尽管如此,当我们遇到这样的惯例时,我们应该遵循它们。 正如我们在第二章、整洁代码原则中提到的,确保熟悉是提高代码可维护性的一个很好的方法。 没有比采用其他程序员已经采用的惯例更好的确保熟悉的方法了。
许多隐含的契约都与类型有关,而 JavaScript(您可能已经知道)是动态类型的。 这意味着值的类型将在运行时确定,并且任何变量所包含的类型都可能发生变化:
var something;
something = 1; // a number
something = true; // a boolean
something = []; // an array
something = {}; // an object
一个变量可以引用许多不同的类型,这意味着我们采用的名称所隐含的契约和约定更加重要。 没有静态类型检查来帮助我们。 我们只能独自面对自己和其他程序员的突发奇想。
Later in this chapter, we'll discuss Hungarian notation, a type of naming that is useful in dynamically typed languages. Also, it's useful to know that there are various static type checking and type annotating tools available for JavaScript if you find dealing with its dynamism painful. These will be covered in Chapter 15, Tools for Cleaner Code.
契约之所以重要,不仅仅是因为 JavaScript 的动态类型特性。 它们从根本上来说很有用,可以让我们对特定值的行为有信心,以及在整个程序运行时可以从它们得到什么有信心。 想象一下,如果有一个 API,它有一个名为getCurrentValue()
的方法,但并不总是返回当前值。 这将破坏其隐含的契约。 通过合同的透镜看名字是相当扭曲思维的。 很快,您就会看到契约无处不在——变量之间的契约、接口之间的契约,以及在整个体系结构和系统之间的集成级别上的契约。
现在我们已经讨论了好名称的三个特征(目的、概念、契约),我们可以开始研究一些反模式,也就是应该尽量避免的事物的命名方法。
命名的反模式
与 DRY 和 YAGNI 的抽象构建警告非常相似,命名也有自己的警告和反模式。 有很多方法可以组成一个坏名声,和几乎所有的他们可以分成三大命名反模式:不必要的短名称、不必要的异国情调的名字,和不必要地长名称。****
名字是最初的镜头,通过它,我们和其他人将看到我们构建的抽象。 因此,知道如何避免创建只会模糊其他程序员理解和使事情复杂化的镜头是至关重要的。 让我们从探索那些不必要的短名字开始,以及它们如何极大地限制了我们理解事物的能力。
不必要的短名称
太短的名称通常使用特定于程序的知识或特定于领域的知识,这可能不能很好地概括代码的受众。 一个单独的程序员可能认为编写以下代码是合理的:
function incId(id, f) {
for (let x = 0; x < ids.length; ++x) {
if (ids[x].id === id && f(ids[x])) {
ids[x].n++;
}
}
}
我们可以看出,它与 id 相关,其目的是在ids
数组中有条件地增加特定对象的n
属性。 因此,我们有可能在功能层面上分辨它在做什么,但它的意义和意图却很难把握。 程序员使用了单字母名称(f
,x
,n
),也使用了缩写函数名(incId
)。 这些名字中的大多数都不能满足我们对名字的基本要求:表明目的、概念和契约。 我们只能通过如何使用这些名称来猜测它们的用途和概念。 用更有意义的名称进行重构会有很大帮助:
function incrementJobInstancesByIdIfFilter(id, filter) {
for (let i = 0; i < jobs.length; i++) {
let job = jobs[i];
if (job.id === id && filter(job)) {
job.nInstances++;
}
}
}
我们现在对正在发生的事情有了更清楚的了解。 被迭代的数组包含作业。 该函数的目的是查找具有指定 ID 和满足指定筛选器条件的作业。 它将作业的nInstances
属性加1
。 通过这些新名称,我们已经对这个抽象概念有了更丰富的概念理解。 现在我们知道作业是可以有任意数量实例的项,并且当前实例的数量是通过nInstances
属性跟踪的。 通过名称提供的透镜,我们已经能够更清楚地理解潜在的问题领域。 现在,我们可以看到名称不仅仅是装饰或不必要的冗长; 名字是你抽象的本质。
一个不必要的短名字,在很多方面,只是一个不够有意义的名字。 然而,名字短并不一定意味着有问题。 我们在前面的代码中使用的迭代器变量i
是完全正确的,因为它是几十年来形成的一种习惯。 全世界的程序员都理解它的概念和契约含义:它仅用于遍历数组,并在迭代的每个阶段访问数组元素。
总的来说,除了像迭代变量这样的罕见例外,避免短名称带来的含义上的缺陷是非常重要的。 它们最初通常由匆忙或懒惰组成,甚至可能给符合其含义的程序员一种成就感。 毕竟,能够运用晦涩的逻辑是给自我的礼物。 但是正如我们所提到的,自我并不是清洁代码的朋友。 当你想用一个简短的名字的时候,抑制住这种冲动,花点时间选一个含义更丰富的名字。
不必要的异国情调的名字
另一个自我膨胀的途径是外来名字的激增。 异国情调的名字往往会引起不必要的注意,意思往往晦涩难懂,比如:
function deStylizeParameters(params) {
disEntangleParams(params, p => !!p.style).obliterate();
}
这是一个表面上简单的行为,被不必要的异国名字所掩盖。 我们可以,用最少的努力,让这些抽象的可理解性变得不同,只需要做一些调整:
function removeStylingFromParams(params) {
filterParams(params, param => !!param.style).remove();
}
总的来说,名字应该很无聊。 他们不应该引起别人的注意。 他们应该坐在那里,只显示他们的简单含义,而不是让其他程序员去,哦,这就是它的意思! 或呵呵聪明! 我们的自我可能对命名有自己的想法,但我们应该记住限制自我,只考虑那些必须忍受努力理解我们的代码和我们创建的接口的任务的人。 总的来说,以下建议将使我们在正确的轨道上:
- 避免使用常规词的花哨或较长的同义词:例如,用
kill
或obliterate
代替delete
避免使用以外的词:例如deletify
、elementize
、dedupify
:例如,使用化学元素名来指代 DOM 元素
*过于异域化可能会疏远我们的观众。 您可能能够很容易地理解您所采用的名称,但这并不意味着其他人也能很容易地理解它们。 更广泛的编程社区是非常多样化的,有许多不同的文化和语言背景。 最好坚持使用描述性和枯燥的名称,以便尽可能多的人能够理解您的代码。
不必要地长名字
正如我们已经发现的,这个不必要的短名字实际上是一个没有足够意义的名字。 因此,不必要的长名字是一个有太多含义的名字。 你可能会想,一个名字怎么会有这么多含义呢? 意义是一件好事,但太多的意义挤在一个名字里只会让人感到困惑; 例如:
documentManager.refreshAndSaveSignedAndNonPendingDocuments();
这个名称很难理解:它是在刷新和保存已签名的文档和非挂起的文档,还是同时刷新和保存已签名和非挂起的文档? 目前尚不清楚。
这个长名字给了我们一个线索,即底层抽象是不必要的复杂。 我们可以将名称分解成它的组成部分,以便全面了解它的界面:
- 刷新(动词):发生在文档上的刷新动作
- save(动词):保存文件的动作
- signed(形容词
- non-pending(形容词):文件的非未决状态
- document(名词
这里发生了一些不同的事情。 对于这么长的名称,一个好的指导方针是重构底层抽象,这样我们最多只需要一个名称带有一个动词、一个形容词和一个名词。 例如,我们可以使用长名称并将其函数分解为四个不同的函数:
documentManager.refreshSignedDocuments();
documentManager.refreshNonPendingDocuments();
documentManager.saveSignedDocuments();
documentManager.saveNonPendingDocuments();
或者,如果意图是在带有多个状态(SIGNED
和NON_PENDING
)的文档上执行操作,那么我们可以实现这样的方法来刷新(和类似的方法来保存操作):
documentManager.refreshDocumentsWithStates([
documentManager.STATE_SIGNED,
documentManager.STATE_NON_PENDING
]);
重点是,长名字是一个线索,以打破或混淆抽象。 让一个名字更容易理解通常与让抽象更容易理解密切相关。
与短名称一样,问题不在于名称本身的长度:而在于名称的长度通常表示什么。 对于较长的名称,所表明的是将过多的含义压缩到单个名称中,表明一种令人困惑的抽象。
一致性和层次结构
到目前为止,我们已经讨论了名字的三个最重要的特征:目的,概念,和合同。 赋予你的名字这些特征的最简单的方法之一就是使用一致性和等级来为你的利益服务。 这里的一致性指的是在给定的代码区域内,在许多不同的名称之间使用相同的命名模式。 另一方面,层次结构指的是我们将不同区域的代码组织在一起以形成一个整体架构的方式。 它们一起允许我们给一个名称提供丰富的上下文,可以用来对其目的、概念和契约作出强有力的推论。
这最好通过查看一个虚构应用的 JavaScript 目录来解释。我们有一个充满文件的目录,像这样:
app/
|-- deepClone.js
|-- deepEquality.js
|-- getParamsFromURL.js
|-- getURL.js
|-- openModal.js
|-- openModalWithTemplate.js
|-- setupAppWithCustomConfig.js
|-- setupAppWithDefaultConfig.js
|-- setURL.js
|-- ...
这里没有层级,所以我们只能从名字本身以及它们似乎与什么相关来辨别上下文。 例如,有一个getURL
和setURL
文件,这两个文件可能都与 url 有关,可以认为是实用程序。 因此,让它们占据层次结构的相同部分或共享名称空间(如app/utils/url
)将会很有帮助。 我们还可以重构目录结构的其他部分,使其成为上下文更丰富的层次结构:
app/
|-- setup/
| |-- defaultConfig.js
| |-- setup.js
|-- modal/
| |-- open.js
| |-- openWithTemplate.js
|-- utils/
|-- url/
| |-- getParams.js
| |-- get.js
| |-- set.js
|-- obj/
|-- deepEquality.js
|-- deepClone.js
事情马上就清楚了。 由于每个文件都有自己的丰富上下文,理解所有这些文件及其作用的认知压力现在减轻了。 您还会注意到,我们已经能够在层次结构的不同部分简化名称; 例如,我们将openModal.js
重命名为modal/open.js
。 这是使用名称层次结构的另一个好处:在每一层命名中,我们都可以简化和缩短名称,减少理解时间。
Names within a hierarchy naturally receive a portion of their meaning from the context that they reside in. This means that the name itself does not need to contain all the meaning. Always look for opportunities to provide a common context to similar abstractions so that the burden of comprehension is eased.
正如我们通过目录结构的层次结构来提供意义一样,我们也可以在代码本身中提供意义。 例如,在一个函数中,函数名自然会从函数名本身和它在更大的模块中的情况中接收很多上下文。 想想这样写代码是多么不寻常:
function displayModalWithMessage(
modalDisplayer_Message,
modalDisplayer_Options
) {
const modalDisplayer_ModalInstance = new Modal();
modalDisplayer_ModalInstance.setMessage(modalDisplayerMessage);
modalDisplayer_ModalInstance.setOptions(modalDisplayerOptions);
modalDisplayer_ModalInstance.show();
return modalDisplayer_ModalInstance;
}
函数中的名称没有必要加上上下文信息(例如modalDisplayer_...
)作为前缀,代码的读者已经可以从函数本身获得这些信息。 通常,我们编写的代码会利用变量所在的位置以及它从上下文获得的含义。 如果前面的代码是这样的,那就正常多了:
function showModalWithMessage(message, options) {
const modalInstance = new Modal();
modalInstance.setMessage(message);
modalInstance.setOptions(options);
modalInstance.show();
return modalInstance;
}
在前一章中,我们讨论了抽象原则,以及模块的实现应该如何独立于接口。 我们可以看到这个原理用这个函数表示。 函数的作用域(它的实现)应该完全独立于它的接口。 因此,可以论证的是,变量不需要知道它驻留在哪个函数中,所以前面的命名技术,即在它前面加上modalDisplayer_...
,将违反抽象原则。
从抽象的角度考虑层次结构是关键。 等级制度不仅仅从组织的角度来看是有用的。 理想情况下,它们应该是驻留在代码中的抽象层的反映。 更高级别的抽象位于层次结构的顶端,我们越深入层次结构,就会得到越低的层次。 这是一个很好的通用规则:让你的层次反映你的抽象。
以一致性命名事物是对该规则的补充。 在抽象的单个层中,也就是在层次结构的单个层中,我们应该采用通用的命名模式,以便代码的读者能够轻松地导航和理解其概念。 例如,如果我们正在创建一个用于从数据结构中添加和删除项的接口,那么我们应该避免以不一致的方式命名类似的操作。 考虑下面的类原理图:
class MyDataStructure {
addItem() {}
pushItems() {}
setItemIfNotExists() {}
// ...
}
非常令人困惑的是,这个抽象提供了向数据结构添加的概念的三个不同变体:添加、推和设置。 这些名称实际上都是指同一个概念,所以我们应该采用一种通用的命名模式,例如:
class MyDataStructure {
addItem() {}
addItems() {}
addItemIfNotExists() {}
// ...
}
这个界面现在更容易理解了。 在使用它时,歧义和认知负担更少。 作为这个抽象的用户,我将不再需要记住我是否应该使用add,set,或push。 一致性是避免不必要的差异的结果。 不一致是不和谐的,所以它们只能用来区分真正的功能或概念上的差异。
技巧和注意事项
JavaScript,由于其不断变化的特性,已经收集了大量冲突的约定。 这些公约中有许多得到了强烈支持或反对的意见。 然而,我们已经确定了一些基本的命名惯例,这些惯例或多或少被全球接受:
- 常量应该用下划线分隔的大写字母命名; 例如:
DEFAULT_COMPONENT_COLOR
- 构造函数或类应该使用大写字母开头的驼峰形式; 例如:
MyComponent
- 其他所有内容都应该以小写字母开头,以驼峰形式显示; 例如:
myComponentInstance
除了这些基本约定之外,命名的决定在很大程度上取决于程序员的创造力和技能。 你最终使用的名字很大程度上取决于你要解决的问题。 大多数代码将继承与其接口的 api 的命名约定。 例如,使用 DOM API 通常意味着采用元素、属性和节点等名称。 许多流行的框架都倾向于指定我们所采用的名称。 从您所从事的生态系统中采用这些传统的范例是绝对有用和必要的,但拥有一些基础技术和概念也是非常有用的,这样您就可以创建命名精美的抽象,即使是在新的和陌生的问题领域。
匈牙利命名法
JavaScript 是一种动态类型语言,这意味着值的类型将在运行时确定,任何变量所包含的类型都可能在运行时发生变化。 这与静态类型语言相反,静态类型语言具有与类型使用相关的编译时警告。 这意味着,作为 JavaScript 程序员,我们需要在使用类型和命名变量的方式上更加小心。
我们知道,当我们命名事物时,我们是在暗示一份合同。 这个契约将定义其他程序员如何使用它。 这就是为什么在各种语言中,一种叫做匈牙利表示法的东西非常流行的部分原因。 它包括在名称本身中包含类型注释,比如:
- 我们可以用
elButton
或buttonElement
代替button
- 我们可以用
nAge
或ageNumber
代替age
- 我们可以用
objDetails
或detailsObject
代替details
匈牙利表示法很有用,原因如下:
- 确定性:它为代码的读者提供了一个名称的目的和契约的更多确定性
- 一致性:它导致了更一致的命名方法
- 强制:它可能导致在代码中更好地强制键入约定
然而,它也有以下缺点:
- Runtime changes:如果底层类型在运行时被错误代码改变(例如,如果一个函数将
nAge
变成了一个字符串),那么该名称将不再有用,可能只会误导我们。 - 代码库刚性:它可能导致代码库僵化,很难对类型做出适当的更改。 重构旧代码可能会变得更麻烦。
- :只知道变量的类型并不能像真正描述性的非类型化变量名那样告诉我们它的目的、概念或契约。
在 JavaScript 中,我们看到少数地方使用了匈牙利表示法:最常见的是在命名可能引用 DOM 元素的变量时。 这些名称的表示法通常为elHeader
、headerEl
、headingElement
甚至$header
。 后者带有美元前缀,在 jQuery 库中使用得最为出名。 它在那里的名声使它成为其他许多地方的标准。 例如:Chromium DevTools在元素引用和查询 DOM 相关的方法中使用了一个 dollar 前缀(例如:$$(...)
别名为document.querySelectorAll(...)
)。
匈牙利符号可以部分使用,如果你担心会有歧义。 例如,当复杂类型和基元类型在单个作用域内引用同一概念时,可以使用它:
function renderArticle(name) {
const article = Article.getByName(name);
const title = article.getTitle();
const strArticle = article.toString();
// ...
}
在这里,我们有一个article
变量,它引用了Article
类的实例。 除此之外,我们还希望使用文章的字符串表示。 为了避免潜在的命名冲突,我们使用了一个str
前缀来表示变量指向一个字符串值。 在像这样的孤立情况下,匈牙利符号可能是有用的。 你不需要穷尽地使用它,但它是你袖子里有用的工具。
*# 命名和抽象函数
在 JavaScript 中创建的大多数抽象都将在函数中表现出来。 即使在宏伟的建筑中,个体的功能和方法也在发挥作用,在他们的概念中,一个好的抽象开始显现出来。 因此,我们应该如何命名我们的功能,以及在命名时我们应该考虑哪些因素,这是值得深思的。
函数的名称通常使用语法中称为祈使句的形式。 祈使句是我们作指示时使用的,例如:walk to The shop,buy bread,stop there! 。
Although we usually use the imperative form when naming functions, there are exceptions. For example, it is also conventional to prefix functions that return Boolean values with is or has; for example,isValid(...)
. When creating constructors (which are functions), we name them according to the instance they'll produce; for example, Route
or SpecialComponent
.
命令式的直接性质在编程环境中是最容易理解和可读性的。 为了为你的特定问题找到正确的祈使句形式,最好是想象下下军事命令的行为,也就是说,不要拐弯抹角地说出你想要发生的事情:
- 如果您想显示提示符,请使用
displayPrompt()
- 如果你想删除元素,使用
removeElements()
- 如果你想要一个在
x
和y
之间的随机数,使用generateRandomNumber(x, y)
通常,我们希望限定我们的指示。 如果你向一个人发出一个指令,如找我的自行车,你可能会进一步限定,指令信息,如它是蓝色的和它失踪前轮【5】。 但是,重要的是,不要让函数名陷入这些限制。 下面的函数就是一个例子:**
findBlueBicycleWithAMissingFrontWheel();
正如我们前面提到的,不必要的长名称是错误抽象的标志。 当我们看到这种类型的资历过高时,我们应该后退一步,重新考虑。 在这里,重要的是要在口头语言的合理和编程时的合理之间划一条界线。 在编程中,函数是抽象常见行为的方法,可以根据需要通过参数调整或配置这些行为。
因此,我们应该通过论证来表达blue
和missing
前轮的资格。 我们可以,例如,把这些表示为一个单一的对象参数,像这样:
findBicycle({
color: 'blue',
frontWheel: 'missing'
});
通过将函数名的限定部分移动到它的参数中,我们产生了一个更清晰和更容易理解的抽象。 这样做的好处是增加了抽象的可配置性,从而为用户提供了更多的可能性。
在我们的例子中,我们可能希望让用户能够找到除自行车之外的其他对象。 为了满足这一点,我们将使函数的名称更通用(例如,findObject
),并通过添加一个新的选项属性(例如,type
)将限定符转移到参数,如下所示:
findObject({
type: 'bicycle',
color: 'blue',
frontWheel: 'missing'
});
在这个过程的这一阶段发生了一些奇怪的事情。 我们已经正确地将各种限定符移动到函数的参数中,扩展了抽象的有用性和配置。 但是现在我们所拥有的是一个做很多事情的抽象,所以在某些情况下,可能要谨慎地后退一步,构建更高级别的抽象来封装这些不同的行为。 在我们的例子中,我们可以通过功能组合来实现这一点,像这样:
const findBicycle = config => findObject({ ...config, type: 'bicycle' });
const findSkateboard = config => findObject({ ...config, type: 'skateboard' });
const findScooter = config => findObject({ ...config, type: 'scooter' });
最重要的是,函数是行为单位。 正如 SRP 告诉我们的,确保他们只做一件可识别的事情是很重要的。 当考虑这些东西或行为单位时,从使用它的人的角度考虑函数的作用是很重要的。 从技术上讲,我们的组合findScooter
函数很可能在表面下做所有的事情。 它可能非常复杂。 但是在使用它的抽象层中,它只能做一件事,那就是最重要的。
三个坏名字
如果你一直想不起一个名字,有一个聪明的方法可以让你摆脱这个困扰。 当你有一个抽象或变量需要一个名字时,仔细看看它做了什么或者它包含了什么,然后想出至少三个不好的名字来描述它。 现在不要担心您希望提供的抽象或接口; 想象一下,您正在向一个对代码库一无所知的人描述功能。 直接和描述性。
例如,假设我们嵌入到代码库中用于设置新用户名的部分。 我们需要检查用户名是否与一组特别禁止的单词(如admin
、root
或user
)匹配。 我们想写一个函数来做这个,但我们不确定选择什么名字。 所以,我们决定试试这三个坏名字的方法。 这就是我们的想法:
matchUsernameAgainstForbiddenWords
checkForForbiddenWordConflicts
isUsernameReservedWord
想出三个不太完美的名字要比花上几分钟也想不出一个完美的名字容易得多。 不管这三个名字有多烂。 重要的是我们至少能想出三个。 现在,在确定了一系列可能性之后,我们可以自由地比较和对比我们找到的名称,并混合和匹配它们,以找到描述函数目的的最具描述性和最直接的方式。 在这种情况下,我们可能最终决定从这三种可能性中改编一个名字:isUsernameForbiddenWord
。 如果不是因为这三个坏名字,我们就不会有今天的成就。
总结
在本章中,我们讨论了命名事物这门困难的艺术。 我们已经讨论了一个好名字的特征,即目的、概念和契约。 我们已经通过示例介绍了如何将这些特征编织到我们的名字中,以及应该避免哪些反模式。 我们还讨论了层次结构和一致性在追求清晰抽象中的重要性。 最后,我们还介绍了一些有用的技术和约定,当我们遇到命名困难时可以使用这些技术和约定。
在下一章中,我们将最终开始深入研究 JavaScript 语言本身的内部,并学习如何以一种产生真正干净代码的方式使用它的结构和语法。*
版权属于:月萌API www.moonapi.com,转载请注明出处