Decorator is a structural pattern that consists of dynamically augmenting the behavior of an existing object. It's different from classical inheritance, because the behavior is not added to all the objects of the same class but only to the instances that are explicitly decorated.
Implementation-wise, it is very similar to the Proxy pattern, but instead of enhancing or modifying the behavior of the existing interface of an object, it augments it with new functionalities, as described in the following figure:
In the previous figure, the Decorator
object is extending the Component
object by adding the methodC()
operation. The existing methods are usually delegated to the decorated object, without further processing. Of course, if necessary we can easily combine the Proxy pattern, so that also the calls to the existing methods can be intercepted and manipulated.
Although Proxy and Decorator are conceptually two different patterns, with different intents, they practically share the same implementation strategies. Let's revise them.
Using composition, the decorated component is wrapped around a new object that usually inherits from it. The Decorator in this case simply needs to define the new methods while delegating the existing ones to the original component:
function decorate(component) { var proto = Object.getPrototypeOf(component); function Decorator(component) { this.component = component; } Decorator.prototype = Object.create(proto); //new method Decorator.prototype.greetings = function() { //... }; //delegated method Decorator.prototype.hello = function() { this.component.hello.apply(this.component, arguments); }; return new Decorator(component); }
Object decoration can also be achieved by simply attaching new methods directly to the decorated object, as follows:
function decorate(component) {
//new method
component.greetings = function() {
//...
};
return component;
}
The same caveats discussed during the analysis of the Proxy pattern are also valid for Decorator. Let's now practice the pattern with a working example!
Before we start coding with the next example, let's spend a few words to introduce LevelUP, the module that we are now going to work with.
LevelUP (https://npmjs.org/package/levelup) is a Node.js wrapper around Google's LevelDB, a key-value store originally built to implement IndexedDB in the Chrome browser, but it's much more than that. LevelDB has been defined by Dominic Tarr as the "Node.js of databases", because of its minimalism and extensibility. Like Node.js, LevelDB provides blazing fast performances and only the most basic set of features, allowing developers to build any kind of database on top of it.
The Node.js community, and in this case Rod Vagg, did not miss the chance to bring the power of this database into Node.js by creating LevelUP. Born as a wrapper for LevelDB, it then evolved to support several kinds of backends, from in-memory stores, to other NoSQL databases such as Riak and Redis, to web storage engines such as IndexedDB and localStorage, allowing to use the same API on both the server and the client, opening up some really interesting scenarios.
Today, there is a full-fledged ecosystem around LevelUp made of plugins and modules that extend the tiny core to implement features such as replication, secondary indexes, live updates, query engines, and more. Also, complete databases were built on top of LevelUP, including CouchDB clones such as PouchDB (https://npmjs.org/package/pouchdb) and CouchUP (https://npmjs.org/package/couchup), and even a graph database, levelgraph
(https://npmjs.org/package/levelgraph) that can work both on Node.js and the browser!
Find out more about the LevelUP ecosystem at https://github.com/rvagg/node-levelup/wiki/Modules.
In the next example, we are going to show how we can create a simple plugin for LevelUp using the Decorator pattern, and in particular, the object augmentation technique, which is the simplest but nonetheless the most pragmatic and effective way to decorate objects with additional capabilities.
For convenience, we are going to use the level
package (http://npmjs.org/package/level) that bundles both levelup
and the default adapter called leveldown
, which uses LevelDB as the backend.
What we want to build is a plugin for LevelUP that allows to receive notifications every time an object with a certain pattern is saved into the database. For example, if we subscribe to a pattern such as {a: 1}
, we want to receive a notification when objects such as {a: 1, b: 3}
or {a: 1, c: 'x'}
are saved into the database.
Let's start to build our small plugin by creating a new module called levelSubscribe.js
. We will then insert the following code:
module.exports = function levelSubscribe(db) { db.subscribe = function(pattern, listener) { //[1] db.on('put', function(key, val) { //[2] var match = Object.keys(pattern).every(function(k) { //[3] return pattern[k] === val[k]; }); if(match) { listener(key, val); //[4] } }); }; return db; }
That's it for our plugin, and it's extremely simple. Let's see what happens in the preceding code briefly:
db
object with a new method named subscribe()
. We simply attached the method directly to the provided db
instance (object augmentation).put
operation performed on the database.Let's now create some code—in a new file named levelSubscribeTest.js
—to try out our new plugin:
var level = require('level'); //[1] var db = level(__dirname + '/db', {valueEncoding: 'json'}); var levelSubscribe = require('./levelSubscribe'); //[2] db = levelSubscribe(db); db.subscribe({doctype: 'tweet', language: 'en'}, //[3] function(k, val){ console.log(val); }); //[4] db.put('1', {doctype: 'tweet', text: 'Hi', language: 'en'}); db.put('2', {doctype: 'company', name: 'ACME Co.'});
This is what we did in the preceding code:
db
object.subscribe()
method, where we specify that we are interested in all the objects with doctype: 'tweet'
and language: 'en'
.db.put('1', {doctype: 'tweet', text: 'Hi', language: 'en'}); db.put('2', {doctype: 'company', name: 'ACME Co.'});
This example shows a real application of the decorator pattern in its most simple implementation: object augmentation. It might look like a trivial pattern but it has undoubted power if used appropriately.
For simplicity, our plugin will work only in combination with the put
operations, but it can be easily expanded to work even with the batch
operations (https://github.com/rvagg/node-levelup#batch).
For more examples of how Decorator is used in the real world, we might want to inspect the code of some more LevelUp plugins:
level-inverted-index
(https://github.com/dominictarr/level-inverted-index): This is a plugin that adds inverted indexes to a LevelUP database, allowing to perform simple text searches across the values stored in the databaselevel-plus
(https://github.com/eugeneware/levelplus): This is a plugin that adds atomic updates to a LevelUP database