揭示构造器
Revealing Constructor 模式是您在 “Gang of Four” 书中找不到的模式之一,因为它直接源自 JavaScript 和 Node.js 社区。 它解决了一个非常棘手的问题,即:我们如何才能仅在对象创建时“揭示”对象的某些私有功能? 当我们希望仅在对象的创建阶段对其内部进行操作时,这特别有用。 这允许一些有趣的场景,例如:
-
创建只能在创建时修改的对象
-
创建只能在创建时定义自定义行为的对象
-
创建只能在创建时初始化一次的对象
这些只是 揭示构造函数模式启用了一些可能性。 但为了更好地理解所有可能的用例,让我们通过查看以下代码片段来了解该模式的含义:
// (1) (2) (3)
const object = new SomeClass(function executor(revealedMembers) {
// manipulation code ...
})
从前面的代码中我们可以看到,揭示构造函数模式由三个基本元素组成: 将函数作为输入的构造函数 (1)(执行程序 (2)),该构造函数在创建时调用并接收对象内部的子集作为输入(显示成员 (3))。
为了使该模式发挥作用,一旦创建了对象,所显示的功能就必须无法被对象的用户访问。 这可以通过我们在上一节中提到的有关工厂模式的封装技术之一来实现。
Domenic Denicola 是第一个在他的一篇博文中识别并命名该模式的人,该博文可以在 nodejsdp.link/domenic-revealing-constructor 上找到。 |
现在,让我们看几个示例,以更好地理解揭示构造函数模式的工作原理。
构建一个不可变缓冲区
不可变对象和数据结构具有许多优秀的属性,使它们非常适合在无数情况下代替可变(或可更改)的对象。 不可变是指对象的属性,一旦创建,其数据或状态就变得不可修改。
对于不可变对象,我们不需要在将它们传递给其他库或函数之前创建防御性副本。 根据定义,我们只是有一个强有力的保证,即使它们被传递给我们不知道或控制的代码,它们也不会被修改。
修改不可变对象只能通过创建新副本来完成,并且可以使代码更易于维护且更易于推理。 我们这样做是为了更容易跟踪状态变化。
不可变对象的另一个常见用例是高效的变化检测。 由于每个更改都需要一个副本,并且如果我们假设每个副本都对应于一个修改,那么检测更改就像使用严格相等运算符(或三重等于 ===)一样简单。 该技术广泛应用于前端编程中,以有效检测 UI 是否需要刷新。
在这种情况下,现在让我们使用 Revealing Constructor 模式创建 Node.js Buffer 组件 (nodejsdp.link/docs-buffer) 的简单不可变版本。 该模式允许我们仅在创建时操作不可变的缓冲区。
让我们在一个名为 immutableBuffer.js 的新文件中实现不可变缓冲区,如下所示:
const MODIFIER_NAMES = ['swap', 'write', 'fill']
export class ImmutableBuffer {
constructor (size, executor) {
const buffer = Buffer.alloc(size) // (1)
const modifiers = {} // (2)
for (const prop in buffer) { // (3)
if (typeof buffer[prop] !== 'function') {
continue
}
if (MODIFIER_NAMES.some(m => prop.startsWith(m))) { // (4)
modifiers[prop] = buffer[prop].bind(buffer)
} else {
this[prop] = buffer[prop].bind(buffer) // (5)
}
}
executor(modifiers) // (6)
}
}
现在让我们看看新的 ImmutableBuffer 类是如何工作的:
-
首先,我们分配一个新的 Node.js Buffer(缓冲区),其大小在构造函数参数中指定。
-
然后,我们创建一个对象文字(修饰符)来保存所有可以改变缓冲区的方法。
-
之后,我们迭代内部缓冲区的所有属性(自己的和继承的),确保跳过所有非函数的属性。
-
接下来,我们尝试确定当前的 prop 是否是允许我们修改缓冲区的方法。 我们通过尝试将其名称与 MODIFIER_NAMES 数组中的字符串之一匹配来实现这一点。 如果我们有这样的方法,我们将它绑定到缓冲区实例,然后将它添加到修饰符对象。
-
如果我们的方法不是修饰符方法,那么我们直接将其添加到当前实例(this)。
-
最后,我们调用构造函数中作为输入接收的执行程序函数,并将修饰符对象作为参数传递,这将允许执行程序改变我们的内部缓冲区。
实际上,我们的 ImmutableBuffer 充当其使用者和内部缓冲区对象之间的代理。 buffer 实例的一些方法直接通过 ImmutableBuffer 接口公开(主要是只读方法),而另一些则提供给执行器函数(修饰符方法)。
我们将在第 8 章 “结构设计模式” 中更详细地分析代理模式。
请记住,这只是揭示构造函数模式的演示,因此不可变缓冲区的实现有意保持简单。 例如,我们不会公开缓冲区的大小或提供其他方法来初始化缓冲区。 我们将把这个留给您作为练习。 |
现在,让我们编写一些代码来演示如何使用新的 ImmutableBuffer 类。 让我们创建一个新文件 index.js,其中包含以下代码:
import { ImmutableBuffer } from './immutableBuffer.js'
const hello = 'Hello!'
const immutable = new ImmutableBuffer(hello.length,
({ write }) => { // (1)
write(hello)
})
console.log(String.fromCharCode(immutable.readInt8(0))) // (2)
// the following line will throw
// "TypeError: immutable.write is not a function"
// immutable.write('Hello?') // (3)
从前面的代码中我们可以注意到的第一件事是执行器函数如何使用 write() 函数(它是修饰符方法的一部分)将字符串写入缓冲区 (1)。 以类似的方式,执行器函数可以使用 fill()、writeInt8()、swap16() 或修饰符对象中公开的任何其他方法。
我们刚刚看到的代码还演示了新的 ImmutableBuffer 实例如何仅公开不改变缓冲区的方法,例如 readInt8() (2),而它不提供任何更改缓冲区内容的方法 (3)。
in the wild
揭示构造函数模式提供了非常强大的保证,因此,它主要用于我们需要提供万无一失的封装的上下文中。 该模式的完美应用应该是在数十万开发人员使用的组件中,这些开发人员必须提供非固定的接口和严格的封装。 但是,我们也可以在项目中使用该模式来提高可靠性并简化与其他人和团队的代码共享(因为它可以使第三方更安全地使用对象)。
揭示构造函数模式的一个流行应用是 JavaScript Promise 类。 你们中的一些人可能已经注意到了。 当我们从头开始创建一个新的 Promise 时,它的构造函数接受一个执行器函数作为输入,该执行器函数将接收用于改变 Promise 内部状态的resolve() 和 reject() 函数。 让我们提醒一下这是什么样子的:
return new Promise((resolve, reject) => {
// ...
})
一旦创建,Promise 状态就不能通过任何其他方式更改。 我们所能做的就是使用我们在第 5 章 “带有 Promises 和 Async/Await 的异步控制流模式” 中已经了解的方法来接收其履行值或拒绝原因。