The revealing constructor pattern is a relatively new pattern that is gaining traction in the Node.js community and in JavaScript, especially because it's used within some core libraries such as Promise
.
We have already implicitly seen this pattern in
Chapter 4, Asynchronous Control Flow Patterns with ES2015 and Beyond, while exploring promises, but let's get back to it and analyze the Promise
constructor to embrace it in greater detail:
const promise = new Promise(function (resolve, reject) { // ... });
As you can see, Promise
accepts a function as a constructor argument, which is called the executor function. This function is called by the internal implementation of the Promise
constructor and it is used to allow the constructing code to manipulate only a limited part of the internal state of the promise under construction. In other words, it serves as a mechanism to expose the resolve
and reject
functions so that they can be invoked to change the internal state of the object.
The advantage of this is that only the constructing code has access to resolve
and reject
and once the promise
object is constructed, it can be passed around safely; no other code will be able to call reject
or resolve
and change the internal state of the promise.
That is exactly the reason why this pattern has been named "revealing constructor" by Domenic Denicola in one of his blog posts.
The full post by Domenic is extremely interesting and also analyzes the historical origins of the pattern and compares some aspects of this pattern with the template pattern used by node streams, or with other construction patterns used by earlier implementation, of the Promise
libraries. You can read it here: https://blog.domenic.me/the-revealing-constructor-pattern/.
In this paragraph, we are going to use the revealing constructor pattern to build a read-only event emitter, a special kind of event emitter in which is not possible to call the emit
method (apart from within the function passed to the constructor).
Let's write the code for the Roee
(read-only event emitter) class in a file called roee.js
:
const EventEmitter = require('events'); module.exports = class Roee extends EventEmitter { constructor (executor) { super(); const emit = this.emit.bind(this); this.emit = undefined; executor(emit); } };
In this simple class, we are extending the core EventEmitter
class and accepting an executor
function as the only constructor argument.
Inside the constructor, we invoke the super
function to be sure to initialize the event emitter properly by calling its parent constructor, then we save a backup of the emit
function and we remove it by assigning undefined
to it.
Finally, we call the executor
function by passing the emit
method backup as argument.
What is important to understand here is that after undefined
is assigned to the emit
method, it won't be possible to call it anymore from other parts of our code. Our backup version of emit
is defined as a local variable that will be forwarded only to the executor
function. This mechanism allows us to be able to use emit
only within the executor function.
Now let's use this new class to create a simple ticker, a class that emits a tick every second and keeps the count of all the ticks emitted. This will be the content of our new ticker.js
module:
const Roee = require('./roee'); const ticker = new Roee((emit) => { let tickCount = 0; setInterval(() => emit('tick', tickCount++), 1000); }); module.exports = ticker;
As you can see here, the code is very trivial. We instantiate a new Roee
and we pass the logic of event emission within the executor function. Our executor function receives emit
as argument so we can use it to emit a new tick event every second.
Now let's see a quick example on how to use this ticker module:
const ticker = require('./ticker'); ticker.on('tick', (tickCount) => console.log(tickCount, 'TICK')); // ticker.emit('something', {}); <-- This will fail
We use the ticker
object the same as any other event emitter-based object, and we can attach any number of listeners with the on
method, but in this case, if we try to use the emit
method, our code will fail by triggering a TypeError: ticker.emit is not a function
error.
Even if this example plays a nice role in showing how the revealing constructor pattern can be used, it is worth mentioning that this read-only functionality for the event emitter is not completely bulletproof and it is still possible to bypass it in several ways. For example, we can still emit an event on our ticker
instance by using the original emit
prototype directly, as follows:
require('events').prototype.emit.call(ticker, 'someEvent', {});
Even if this pattern is quite interesting and clever, it is really hard to find common use cases apart from the Promise
constructor.
It's worth mentioning that there is a new specification for streams under development that tries to adopt this pattern as a better alternative to the currently used template pattern to be able to describe the behavior of various stream objects: https://streams.spec.whatwg.org.
Also it's important to point out that we already used this pattern in
Chapter 5,
Coding with Streams when we implemented the ParallelStream
class. This class accepts as constructor argument the userTransform
function (the executor function).
Even if in this case the executor function is not called at construction time, but in the internal _transform
method of the stream, the general concept of the pattern remains valid. In fact, this approach allows us to expose some internals of the stream (for example, the push
function) only to the specific logic of the transformation that we want to specify at construction time when creating a new instance of ParallelStream
.