Appearance
Javascript Design Patterns
Factory Pattern
- Allows us to separate the object creation logic from its implementation. Consumer of factory is totally agnostic about how the instace is created.
- Factory allows us to keep constructors (or classes) of objects private, and prevents them from being extended or modified - thus adhering to the concept of small surface area
- The following example gives us better:
- Flexibility. The factory method prevents us from binding our implementation with one particular type of object (as oppposed to using
new
operator in implementation) - Control. An exception can be thrown when something unexpected happens.
- Flexibility. The factory method prevents us from binding our implementation with one particular type of object (as oppposed to using
js
function createImage(name) {
if (name.match(/\.jpeg$/)) {
return new JpegImage(name);
} else if (name.match(/\.gif$/)) {
return new GifImage(name);
} else if (name.match(/\.png$/)) {
return new PngImage(name);
} else {
throw new Exception('Unsupported format');
}
}
Enforcing Encapsulation
Encapsulation refers to the technique of controlling access to some internal details of an object by preventing external code from manipulating them directly (i.e. making private variables in OOP jargon). The interaction with these details happens only through its public interface.
As we can't declare private variables in JavaScript, we could implement encapsulation through function scopes and closures:
js
function createPerson(name) {
const privateProperties = {};
const person = {
setName: function () {
if (!name) {
throw new Error('A person must have a name');
}
privateProperties.name = name;
},
getName: function () {
return privateProperties.name;
},
};
person.setName(name);
return person;
}
In the example above:
- The factory returns a
person
object containing public interfaces to external code. - Internally,
privateProperties
object is created and is only accessible through the interface provided by theperson
object. This is possible thanks to the concept of closures.
An alternate approach in creating private variables recommended by Douglas Crockford: http://crockford.com/javascript/private.html
Example Use Case
In this example we will create a simple code profiler that returns the execution time of code execution. It has two methods:
start()
method that triggers the start of profiling sessionend()
method that termintates the session and log its execution time to the console
js
class Profiler {
constructor(label) {
this.label = label;
this.lastTime = null;
}
start() {
this.lastTime = process.hrtime();
}
end() {
const diff = process.hrtime(this.lastTime);
console.log(
`Timer ${this.label} took ${diff[0]} seconds and ${diff[1]} nanoseconds`,
);
}
}
In a real-world application, we might not want the Profiler
to overcrowd the standard output in a production environment. So the Profiler
would run as normal in a development
environment, but is disabled in a production
environment:
js
function profiler(label) {
if (process.NODE_ENV === 'development') {
return new Profiler(label);
} else if (process.NODE_ENV === 'production') {
return {
start: function () {},
end: function () {},
};
} else {
throw new Error('NODE_ENV must be set');
}
}
The factory above very nicely abstracts the Profiler
object creation logic from its consumer. One thing to highlight is that in the production
case, an object literal (instead of a Profiler instance) with a similar interface signature is returned instead. This is an idea called duck typing - i.e. if it walks and quacks like a duck, it must be a duck. Because of its similar interface signature, the consuming code could run without errors, only that start()
and end()
methods would do nothing.
To complete the example, the following shows how Profiler
might be used:
js
function getRandomArray(len) {
const p = profiler(`Generating an array with ${len} items`);
p.start();
const arr = [];
for (let i = 0; i < len; i++) {
arr.push(Math.random());
}
p.end();
}
getRandomArray(1e6);
Composable Factory Functions
A Composable Factory Function is a type of factory function that can be composed together to build new enhanced factory functions. They allow us to construct objects that inherit behaviours from different sources without the need of building complex hierarchies.
This can be illustrated with an example. Say we want to build a game in which it has multiple characters, each of which has different behaviours. The behaviours include:
- Character: the base character that has a name, life points and current position.
- Mover: a character that is able to move
- Slasher: a character that is able to slash with sword
- Shooter: a character that is able to shoot (if he has bullets)
Based on their behaviours, we can build different characters:
- Runner: a character that can move
- Samurai: a character that can move and slash
- Sniper: a character that can shoot (but cannot move)
- Gunslinger: a character that can move and shoot
- Western Samurai: a character that can move, slash and shoot
To begin with Composable Factory Functions, we will use the stampit
module. The module offers an interface for defining factor functions that can be composed together to build new factory functions.
The Character
factory function:
js
const stampit = require('stampit');
const character = stampit().props({
name: 'anonymous',
lifepoints: 100,
x: 0,
y: 0,
});
Usage example:
js
const c = character();
c.name = 'John';
c.lifepoints = 20;
Similarly, we can create our Mover
factory function:
js
const mover = stampit().methods({
move(xIncr, yIncr) {
this.x += xIncr;
this.y += yIncr;
console.log(`${this.name} moved to [${this.x}, ${this.y}]`);
},
});
Notice that we can access instance properties with this
keyword from inside a method.
Slasher
factory function:
js
const slasher = stampit().methods({
slash(direction) {
console.log(`${this.name} slashed to the ${direction}`);
},
});
Shooter
factory function:
js
const shooter = stampit()
.props({
bullets: 6,
})
.methods({
shoot(direction) {
if (this.bullets > 0) {
--this.bullets;
console.log(`${this.name} shot to the ${direction}`);
}
},
});
Now that we have our base types defined based on behaviours, we can go on to compose them to create new factory functions for each character:
js
const runner = stampit.compose(character, mover);
const samurai = stampit.compose(character, mover, slasher);
const sniper = stampit.compose(character, shooter);
const gunslinger = stampit.compose(character, mover, shooter);
const westernSamurai = stampit.compose(gunslinger, samurai);
stampit.compose
defines a new Composed Factory Function that will produce an object based on the methods and properties of the composed factory functions. This is powerful as it allows us to reason in terms of behaviours rather than in terms of classes.
Usage example:
js
const yojimbo = westernSamurai();
yojimbo.name = 'Yojimbo';
yojimbo.move(3, 7);
yojimbo.slash('left');
yojimbo.shoot('right');
Revealing Constructor Pattern
This pattern can be seen in the design of Promises:
js
const promise = new Promise((resolve, reject) => {});
The Promise accepts a function as a constructor argument, 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. It also serves as a mechanism to expose the resolve
and reject
methods to the constructing code (code that builds the object with new
operator).
An additional advantage is that only the constructing code has access to resolve
and reject
. The newly created promise
object can be passed around safely - no other code will be able to call resolve
or reject
and change the internal state of the promise.
In summary, this pattern involves:
- Passing a function as a constructor argument that will be called within the internal implementation of the constructor class
- Exposing internal methods to the constructing code (e.g.
resolve
andreject
). This is possible because during invocation of the executor function, the appropriate internal methods are passed in.
Example Use Case
In this rather contrived example, we will try to create a read-only event emitter that is only able to emit events within the constructing code.
js
const EventEmitter = require('EventEmitter');
class ReadOnlyEventEmitter extends EventEmitter {
constructor(executor) {
super();
const emit = this.emit.bind(this);
this.emit = undefined;
executor(emit);
}
}
What's happening here:
- We made a duplicate of the
emit
method and stored it inemit
- We remove the instance method
emit
by assigningundefined
to it - Finally we pass in the
emit
function to the executor function. This allows us to be able toemit
events only within the executor function
Using ReadOnlyEventEmitter
to make a simple ticker
js
const ticker = new ReadOnlyEventEmitter((emit) => {
let tickCount = 0;
setInterval(() => {
emit('tick', tickCount++);
}, 1000);
});
ticker.on('tick', (tickCount) => {
console.log(tickCount, 'TICK(s)');
});
ticker.emit('tick', 'something'); // this will fail
In the example:
- The executor function (defined in constructing code) is passed into and invoked within the internal implementation of
ReadOnlyEventEmitter
- An internal function
emit
is exposed to the constructing code
Proxy Pattern
A proxy is an object that controls access to another object, called a subject. The proxy and the subject have identical interfaces, which allows us to swap one for the other. A proxy intercepts all or some of the operations that are meant to be executed on the subject, thus augmenting their behaviour.
A proxy is useful for:
- Data validation: The proxy validates input before forwarding to subject
- Security: The proxy verfifies that an action is authorized before passing to the subject
- Caching: The proxy keeps an internal cache so that the operations are executed on the subject only if the data is not yet present on the cache
- Lazy initialization: If the creation of the subject is expensive, the proxy can delay it to when it's really necessary
- Logging: The proxy intercepts the method invocations and the relative parameters, recording them as they happen
- Remote objects: A proxy can take an object that is located remotely, and make it appear local
It is important to note that in this case, we are not proxying between classes. The proxy pattern involves wrapping actual instances of the subject, thus preserving its state.
Implementing Proxies with Object Composition
Composition is a technique whereby an object is combined with another object for the purpose of extending or using its functionality. In this case, a new object (proxy) with the same interface as the subject is created.
The following example shows a factory that creates a proxy:
js
function createProxy(subject) {
const subjectProto = Object.getPrototypeOf(subject);
function Proxy(subject) {
this.subject = subject;
}
Proxy.prototype = Object.create(subjectProto);
// Proxied method
Proxy.prototype.hello = function () {
return this.subject.hello() + ' world';
};
// Delegated method
Proxy.prototye.goodbye = function () {
return this.subject.goodbye.apply(this.subject, args);
};
return new Proxy(subject);
}
To implement a proxy using composition, we have to intercept the methods we are intereseted in manipulating (hello()
), while simply delegating the rest to the subject directly (goodbye()
).
The example above shows a particular case where the subject has a prototype and we want to maintain the correct prototype chain (i.e. the case of classical inheritance). With this setup, proxy instanceof Subject
will return true
.
The alternate, more immediate approach without the use of inheritance, could look like the following:
js
function createProxy(subject) {
return {
// Proxied method
hello: () => subject.hello() + ' world',
// Delegated method
goodbye: () => subject.goodbye.apply(subject, args),
};
}
As a sidenote, accomplishing the proxy pattern by means of Object Composition could be cumbersome as we have to manually delegate all methods even if we are only interested in proxying one of them. For this, an npm called delegates
could be helpful.
Implementing Proxies with Object Augmentation
Object Augmentation or monkey patching consists of modifying the subject directly by replacing a method with its proxied implementation. Following the same example above, this approach would look like:
js
function createProxy(subject) {
const helloOrig = subject.hello;
subject.hello = () => helloOrig.call(this) + ' world';
return subject;
}
Modifying the subject directly may present some undesirable behaviours as further calls to subject.hello()
(directly without proxying) would return the newly augmented behaviour.
Example Use Case
In this example use case, we will create a proxy to a Writable stream that intercepts all calls to write()
and logging a message every time this happens.
js
function createWritableStreamProxy(writableOrig) {
const proto = Object.getPrototypeOf(writableOrig);
function WritableStreamProxy(writableOrig) {
this.writableOrig = writableOrig;
}
WritableStreamProxy.prototype = proto;
WritableStreamProxy.write = function (chunk, encoding, callback) {
if (!callback && typeof encoding === 'function') {
callback = encoding;
encoding = undefined;
}
console.log('Writing ' + chunk);
return this.writableOrig.write(chunk, encoding, function () {
console.log('Finished writing', chunk);
callback && callback();
});
};
WritableStreamProxy.on = function () {
return this.writableOrig.on.apply(this.writableOrig, args);
};
WritableStreamProxy.end = function () {
return this.writableOrig.end.apply(this.writableOrig, args);
};
return new WritableStreamProxy(writableOrig);
}
What's happening here:
- We created a factory that returns a proxied version of the
writable
object passed in as argument - The
write
method is proxied so that a message could be logged before the subject'swrite
method is invoked - Note that in such asynchronous cases, the proxying of callbacks is necessary as well
- The remaining methods
on
andend
are simply delegated to the originalwritable
object
For the sake of completion, the WritableStreamProxy
could be used as follows:
js
const writable = createWriteStream('test.txt');
const writableProxy = createWritableStreamProxy(writable);
writableProxy.write('First chunk');
writableProxy.write('Second chunk');
writable.write('This is not logged');
writableProxy.end();
Other Forms of Proxying
The proxying pattern can also be referred to as function hooking or Aspect Oriented Programming (AOC). In these cases, such implementations involving the setting of pre- and post- execution hooks for a specific methods. There are some libraries that could help facilitate this:
hooks
hooker
meld
ES2015 Proxy
ES2015 introduced a global object called Proxy. The Proxy API contains a Proxy
constructor and accepts a target
and handler
as arguments.
js
const proxy = new Proxy(target, handler);
The target refers to what we have been referring to as the subject of the proxy. The handler
is a special object that defines the behaviour of the proxy. The handler
object comes with a series of optional methods called trap methods (e.g. get
, set
, apply
, has
) that are automatically called when specific operations are performed on the proxy instance.
For example:
js
const scientist = {
name: 'nikola',
surname: 'tesla'
}
const upperCaseScientist = new Proxy(scientist, {
get: (target, prop) => target[prop].toUpperCase();
})
console.log(upperCaseScientist.name, upperCaseScientist.surname) // prints 'NIKOLA TESLA'
In the example, we are intercepting all access to the properties of the target
object, via the use of trap methods. It's important to note that the ES2015 Proxy API allows intercepting other characteristics of an object beyond just its methods.
Another example:
js
const evenNumbers = new Proxy([], {
get: (target, index) => index * 2,
has: (target, number) => number % 2 === 0,
});
console.log(2 in evenNumbers); // true
console.log(5 in evenNumbers); // false
console.log(evenNumbers[7]); // 14
In the code above, we create a virtual array with no data within. By using trap methods, we are able to intercept access to the array and make it appear to contain even numbers. The get
method intercepts access to the array elements while the has
method intercepts the usage of in
operator to discern if the given numbers (should) exist in the array.
The Proxy API supports a number of other trap methods and can be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Decorator Pattern
The Decorator Pattern is similar to the Proxy Pattern, but instaed of modifying existing interfaces of an object, it augments the subject by adding new functionalities.
Implementing Decorator with Object Composition
The implementation is almost identical to Proxy with Object Composition, only that we are adding new methods rather than proxying existing methods:
js
function decorate(component) {
const proto = Object.getPrototypeOf(component);
function Decorator(component) {
this.component = component;
}
Decorator.prototype = Object.create(proto);
// New method
Decorator.prototype.greetings = function () {
return 'Hi!';
};
// Delegated method
Decorator.prototype.hello = function () {
return this.component.hello.apply(this.component, args);
};
return new Decorator(component);
}
Implementing Decorator with Object Augmentation
js
function decorate(component) {
// new method
component.greetings = () => {
return 'Hi!';
};
return component;
}
Example Use Case
We will create a plugin for LevelUP database module using the Decorator Pattern, with the Object Augmentation technique. The plugin will have the feature of notifying us whenever an object with a user-specified pattern gets inserted into the database. For example if we specify a pattern {a: 1}
, we will receive a notification when objects such as {a: 1, b: 44}
or {a: 1, c: 'x'}
are inserted into database.
js
function levelSubscribe(db) {
db.subscribe = (pattern, listener) => {
db.on('put', (key, value) => {
const match = Object.keys(pattern).every((k) => pattern[k] === value[k]);
if (match) {
listener(key, value);
}
});
};
return db;
}
Some notes:
- We decorated the db object (subject) with a new method
subscribe
- The LevelUP module comes with a feature that emits a
put
event whenever there is a database insertion. The associated callback function is supplied with argskey
andvalue
. Think ofkey
as the id of document whilstvalue
is the object inserted - When there is a match in pattern, the listener supplied to
subscribe
method is called
Using levelSubscribe
:
js
const level = require('level');
let db = level(__dirname + '/db', { valueEncoding: 'json' });
db = levelSubscribe(db);
db.subscribe({ doctype: 'tweet', lang: 'en' }, (key, value) =>
console.log(value),
);
db.put('1', { doctype: 'tweet', lang: 'en', text: 'Hello world!' }); // triggers notification
db.put('2', { doctype: 'company', lang: 'it', name: 'ACME Co.' }); // does not trigger notification
Adapter Pattern
The Adapter Pattern allows us to access the functionality of an object using a different interface. This is again similar to the Proxy Pattern, but instead of retaining the same interface as the subject, it exposes a different interface.
Example Use Case
We will attempt to build an Adapter around the LevelUP API, transforming it into an interface that is compatible with the core fs
module. In particular, every call to readFile()
and writeFile()
will translate to db.get()
and db.put()
. This way, we could use the LevelUP database in place of existing fs
implementations without making significant code changes.
js
const path = require('path');
function createFsAdapter(db) {
const fs = {};
fs.readFile = (filename, options, callback) => {
if(typeof options = 'function') {
callback = options;
options = {};
} else if(typeof options = 'string') {
options = { encoding: options };
}
db.get(
path.resolve(filename),
{ valueEncoding: options.encoding },
(err, value) => {
if(err) {
if(err.type === 'NotFoundError') {
err = new Error(`ENOENT, open "${filename}"`);
err.code = 'ENOENT';
err.errno = 34;
err.path = filename;
}
return callback && callback(err)
}
return callback && callback(null, value)
}
)
}
fs.writeFile = (filename, contents, options, callback) => {
if(typeof options === 'function') {
callback = options;
options = {};
} else if(typeof options === 'string') {
options = { encoding: options };
}
db.put(
path.resolve(filename),
contents,
{ valueEncoding: options.encoding },
callback
)
}
return fs;
}
Note that fs.readFile
and fs.writeFile
are defined in a way that mimics the function signatures from the core fs
module.
In any existing implementations of fs
with readFile
and writeFile
, we could simply replace the fs
module dependency with const fs = createFsAdapter(db)
. With this, the transition from filesystem to LevelUP db can be done without causing any changes in results!
Strategy Pattern
The Strategy pattern enables an object, called the Context to support variations in logic by extracting the variable parts into separate, interchangeable objects, called Strategies. The Context is then able to adapt its behaviour by attaching different Strategies to it. The strategies are usually a family of solutions that address a similar problem, and they implement the same interface - one that is expected by the Context.
A rough example is a Context object called Order
, in which has a method called pay()
. To support different payment methods, we could:
- Use an
if...else
statement within thepay()
method - Delegate the logic of payment method to multiple strategies
The first solution will require modifications to the Order
object to support new payment methods. By using strategies, the Order
object can remain untouched as we add new strategies to support new payment methods.
Example Use Case
Let's consider an object called Config
that holds a set of configuration parameters. The object should:
- be able to provide a simple interface to access these parameters
- allow users to import and export the configuration using a file.
- support different formats, i.e. JSON, INI, YAML
js
const fs = require('fs');
const objectPath = require('object-path');
class Config {
constructor(strategy) {
this.data = {};
this.strategy = strategy;
}
get(path) {
return objectPath.get(this.data, path);
}
set(path, value) {
return objectPath.set(this.data, path, value);
}
read(file) {
this.data = this.strategy.deserialize(fs.readFileSync(file, 'utf8'));
}
save(file) {
fs.writeFileSync(file, this.strategy.serialize(this.data));
}
}
object-path
simply allows us to retrieve properties using dot notation:
js
const obj = {
a: {
b: 123,
c: 333,
},
};
objectPath.get(obj, 'a.b');
// returns 123
Now we have that out of the way, it is clear that the implementation details of serialize
and deserialize
are delegated to strategies. As long as all strategies share the same interface and contain these methods, they can be plugged-in when Config
is constructed.
The strategies might look like the following:
js
const ini = require('ini');
strategies = {
ini: {
deserialize: (data) => ini.parse(data),
serialize: (data) => ini.stringify(data),
},
json: {
deserialize: (data) => JSON.parse(data),
serialize: (data) => JSON.stringify(data),
},
};
And the implementation:
js
const jsonConfig = new Config(strategies.json);
jsonConfig.read('path/to/file.json');
jsonConfig.set('object.prop', 'new value');
jsonConfig.save('path/to/new_file.json');
const iniConfig = new Config(strategies.ini);
iniConfig.read('path/to/file.ini');
iniConfig.set('object.prop', 'new value');
iniConfig.save('path/to/new_file.ini');
Note that we defined only one Config
class, which implements the common parts of our configuration manager. Changing the strategies allowed us to create different Config
instances that support different file formats.
There are alternate approaches in arranging our strategies:
- We could group strategies in two families, one for deserialization and the other for serialization. This would enable us to read and save in different formats.
- We could select strategies dynamically based on file extentions. This would of course require an object that maps extensions to strategies.
The Strategy Pattern might appear in different forms too. In its simplest form, it can appear as functions - function context(strategy) {...}
.
State Pattern
State is a variation of the Strategy Pattern where the strategy changes depending on the state of the context. We have seen previously how a strategy can be selected, and once this selection is done, the strategy statys unchanged for the rest of the context's lifespan. Instead, in the State pattern, the strategy (also called state) is dynamic and can change during the context's lifetime.
Consider a hotel booking application that has a Reservation
object and its 3 scenarios:
- When the reservation is initially created, the user can
confirm()
. At this point, the user cannotcancel()
because it's still not confirmed. However, the user coulddelete()
. - Once the reservation is confirmed, the user could not
confirm()
again. It is possible tocancel()
the reservation, but not possible todelete()
a reservation because it needs to be kept as record. - On the day before the reservation date, it is not possible to
cancel()
the reservation as it is too late.
With the State Pattern, we could implement 3 strategies (or states), each representing a scenario above. The basic idea is that the Reservation
context could easily switch between states, thus triggering the correct set of operations based on the current scenario.
The state transition can be initiated and controlled by the context object, by the client code or by the State
objects themselves. The last option usually provides the best results in terms of flexibility and decoupling, as the context does not have to know about all the possible states and how to transition between them.
Example Use Case
To demonstrate this pattern, we will implement a fail-safe client TCP socket that queues data sent during the time the server is offline, and tries to resend them once the server comes online. In this example, the fail-safe socket will be used by client machines to send resource utilization data at regular intervals.
js
const OfflineState = require('./offlineState');
const OnlineState = require('./onlineState');
class FailSafeSocket {
constructor(options) {
this.options = options;
this.queue = [];
this.currentState = null;
this.socket = null;
this.states = {
offline: new OfflineState(this),
online: new OnlineState(this),
};
this.changeState('offline');
}
changeState(state) {
this.currentState = this.states[state];
this.currentState.activate();
}
send(data) {
this.currentState.send(data);
}
}
module.exports = (options) => {
return new FailSafeSocket(options);
};
Notes:
- The
changeState()
method is responsible for transitioning between states. It callsactivate()
on the target state to launch any starter code in each state. - The
send()
method simply delegates the operation to the currently active state, which adopts a different behaviour based on the online/offline state.
offlineState.js
might look like:
js
const jot = require('json-over-tcp');
class OfflineState {
constructor(failSafeSocket) {
this.failSafeSocket = failSafeSocket;
}
send(data) {
this.failSafeSocket.queue.push(data);
}
activate() {
const retry = () => {
setTimeout(() => {
this.activate();
}, 500);
};
this.failSafeSocket.socket = jot.connect(
this.failSafeSocket.options,
() => {
this.failSafeSocket.socket.removeListener('error', retry);
this.failSafeSocket.changeState('online');
},
);
this.failSafeSocket.socket.once('error', retry);
}
}
Notes:
- Instead of using a raw TCP socket, we are using
json-over-tcp
to conveniently send json data over tcp. - The
activate()
method simply tries to establish a connection every 500ms. It keeps trying until a connection is established, at which point the state offailSafeSocket
changes toonline
.
onlineState()
:
js
class OnlineState {
constructor(failSafeSocket) {
this.failSafeSocket = failSafeSocket;
}
send(data) {
this.failSafeSocket.socket.write(data);
}
activate() {
this.failSafeSocket.queue.forEach((data) => {
this.failSafeSocket.write(data);
});
this.failSafeSocket.queue = [];
this.failSafeSocket.socket.once('error', () => {
this.failSafeSocket.changeState('offline');
});
}
}
How it all comes together:
js
const createFailSafeSocket = require('./failSafeSocket');
const failSafeSocket = createFailSafeSocket({ port: 5000 });
setInterval(() => {
failSafeSocket.send(process.memoryUsage());
}, 1000);
Template Pattern
The Template Pattern is almost identical to the Strategy Pattern. Instead of composing the context and strategies together, the Template Pattern ties them together through classical inheritance. The parent class (or Template) would contain generic methods, while the child classes will have specific behaviours (or template methods).
Both Strategy and Template patterns allow us to change some parts of a context while reusing the common parts. The distinction is that Strategy allows us to do it dynamically (and possibly at runtime), but with Template, the complete structure is determined the moment the child classes are defined. With this, the Template Pattern might be more suitable in situations where we want to create prepackaged variations of an algorithm.
Example Use Case
We will re-use the same configuration manager example in the Strategy Pattern section.
js
const fs = require('fs');
const objectPath = require('object-path');
class ConfigTemplate {
read(file) {
this.data = this._deserialize(fs.readFileSync(file, 'utf8'));
}
save(file) {
fs.writeFileSync(file, this._serialize(this.data));
}
get(path) {
return objectPath.get(this.data, path);
}
set(path, value) {
return objectPath.set(this.data, path, value);
}
_serialize() {
throw new Error('_serialize() must be implemented');
}
_deserialize() {
throw new Error('_deserialize() must be implemented');
}
}
class JsonConfig extends ConfigTemplate {
_serialize(data) {
return JSON.stringify(data);
}
_deserialize(data) {
return JSON.parse(data);
}
}
How the example might be used:
js
const jsonConfig = new JsonConfig();
jsonConfig.read('path/to/file.json');
jsonConfig.set('prop', 'value');
jsonConfig.save('path/to/another/file.json');
Notes:
- In
ConfigTemplate
, we defined both_serialize()
anddeserialze()
as stubs that throw an error if they are not overriden by children classes. - Notice that a
ConfigTemplate
object is never created as it is simply an Abstract Class.
Middleware Pattern
In the enterprise architecture jargon, 'middleware' represents the various software suites that help to abstract lower-level mechanisms, e.g. memory management, network communications. In the context of Nodejs Express, middleware are a set of functions that do just that - abstracting non-essential parts of the core application (authentication, compression/decompression etc.). The defining characteristic of middleware in the Express world is that they are organized as a processing pipeline, where a set of processing units, filters and handlers, in the form of functions, are connected to form an asynchronous sequence. This pattern is used well beyond the boundaries of Express and will be the focus of this section.
Middleware Manager
The set up of this pattern involves a Middleware Manager
which is responsible for organizing and executing the middleware functions. Its implementation details:
- New middleware can be registered by invoking the
use()
function. Usually new middleware are appended at the end of pipeline but this is not a strict rule. - When new data is received for processing, the registered middleware is invoked in an asynchronous sequential execution flow. Each unit receives the result from the previous middleware.
- Each piece of middleware can decide to stop further processing of data by simply not invoking its callback or by passing an error to the callback. An error situation normally triggers the execution of another sequence of middleware dedicated for errors.
There is no strict rule on how the data is processed along the pipeline. Some strategies:
- Augmenting data with additional properties/functions.
- Replacing data with result of some kind of processing.
- Maintaining the immutability of data and always returning fresh copies
Example Use Case
We will implement the middleware pattern for a message bus implementation called ZeroMQ. ZeroMQ is a lightweight messaging library that allows only strings and binary buffers for messages, so any encoding or custom formatting will have to be implemented manually. For this, we will implement a middleware infrastructure to encode/decode JSON messages before and after they get sent through the message bus.
The Middleware Manager:
js
class ZmqMiddlewareManager {
constructor(socket) {
this.socket = socket;
this.inboundMiddleware = [];
this.outboundMiddleware = [];
socket.on('message', (message) => {
this.executeMiddleware(this.inboundMiddleware, {
data: message,
});
});
}
send(data) {
const message = { data: data };
this.executeMiddleware(this.outboundMiddleware, message, () => {
this.socket.send(message.data);
});
}
use(middleware) {
if (middleware.inbound) {
this.inboundMiddleware.push(middleware.inbound);
}
if (middleware.outbound) {
this.outboundMiddleware.unshift(middleware.outbound);
}
}
executeMiddleware(middleware, arg, finish) {
function iterator(index) {
if (index === middleware.length) {
return finish && finish();
}
middleware[index].call(this, arg, (err) => {
if (err) {
return console.log('There was an error: ' + err.message);
}
iterator.call(this, ++index);
});
}
iterator.call(this, 0);
}
}
module.exports = ZmqMiddlewareManager;
Some notes:
- In this scenario, each middleware comes in an inbound-outbound pair and are executed in inverted order. For example, we need to serialize messages, then compress it before sending it over the wire. On the receiving end, decompression needs to happen first before deserializing.
executeMiddleware()
uses a simple implementation of the asynchronous sequential iteration pattern. Each function inmiddleware
array is executed one after another, and the samearg
object is passed to each middleware function. Thearg
object is mutated along the way as we will see next.
JSON Middleware:
js
const json = () => {
return {
inbound: function(message, next) {
message.data = JSON.parse(message.data.toString());
next();
}
outbound: function(message, next) {
message.data = new Buffer(JSON.stringify(message.data));
next();
}
}
}
module.exports = { json };
Implementation on server side:
js
const zmq = require('zmq');
const ZmqMiddlewareManager = require('./zmqMiddlewareManager');
const jsonMiddleware = require('./jsonMiddleware');
const reply = zmq.socket('rep');
reply.bind('tcp://127.0.0.1:5000');
const zmqm = new ZmqMiddlewareManager(reply);
zmqm.use(jsonMiddleware.json())
zmqm.use(() => {
inbound: function(message, next) {
console.log('Received: ', message.data);
if(message.data.action === 'ping') {
this.send({ action: 'pong', echo: message.data.echo });
}
next();
}
})
Notes:
- A message handler is registered as a middleware to send a reply. Because the previous middleware has already deserialized the message, we could conveniently access the
message
object. - Data passed to the
send
method, will be processed by the outbound middleware before going on the wire. - Notice that the
inbound
andoutbound
methods are defined using thefunction
keyword (rather than arrow functions). Arrow functions are bound to its lexical scope, which means that the value ofthis
is the same as in the parent block and cannot be altered. In other words, if we use an arrow function, our middleware will not recognizethis
as an instance ofZmqMiddlewareManager
.
Implementation on client side:
js
const zmq = require('zmq');
const ZmqMiddlewareManager = require('./zmqMiddlewareManager');
const jsonMiddleware = require('./jsonMiddleware');
const request = zmq.socket('req');
request.connect('tcp://127.0.0.1:5000');
const zmqm = new ZmqMiddlewareManager(request);
zmqm.use(jsonMiddleware.json());
zmqm.use({
inbound: function (message, next) {
console.log('Echoed back: ', message.data);
next();
},
});
setInterval(() => {
zmqm.send({ action: 'ping', echo: Date.now() });
}, 1000);
Command Pattern
This pattern seems unecessary at this point in time and will be covered later.