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
newoperator 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
personobject containing public interfaces to external code. - Internally,
privatePropertiesobject is created and is only accessible through the interface provided by thepersonobject. 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.
resolveandreject). 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
emitmethod and stored it inemit - We remove the instance method
emitby assigningundefinedto it - Finally we pass in the
emitfunction to the executor function. This allows us to be able toemitevents 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 failIn the example:
- The executor function (defined in constructing code) is passed into and invoked within the internal implementation of
ReadOnlyEventEmitter - An internal function
emitis 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
writableobject passed in as argument - The
writemethod is proxied so that a message could be logged before the subject'swritemethod is invoked - Note that in such asynchronous cases, the proxying of callbacks is necessary as well
- The remaining methods
onandendare simply delegated to the originalwritableobject
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:
hookshookermeld
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]); // 14In 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
putevent whenever there is a database insertion. The associated callback function is supplied with argskeyandvalue. Think ofkeyas the id of document whilstvalueis the object inserted - When there is a match in pattern, the listener supplied to
subscribemethod 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 notificationAdapter 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...elsestatement 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 123Now 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-tcpto 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 offailSafeSocketchanges 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
ConfigTemplateobject 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 inmiddlewarearray is executed one after another, and the sameargobject is passed to each middleware function. Theargobject 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
messageobject. - Data passed to the
sendmethod, will be processed by the outbound middleware before going on the wire. - Notice that the
inboundandoutboundmethods are defined using thefunctionkeyword (rather than arrow functions). Arrow functions are bound to its lexical scope, which means that the value ofthisis the same as in the parent block and cannot be altered. In other words, if we use an arrow function, our middleware will not recognizethisas 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.