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 the calls to the existing methods can be intercepted and manipulated as well.
Although Proxy and Decorator are conceptually two different patterns with different intents, they practically share the same implementation strategies. Let's review 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) { 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, 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 = () => { //... }; 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 the next example, let's say a few words about 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 blazingly fast performance 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 backend, from in-memory stores, to other NoSQL databases such as Riak and Redis, to web storage engines such as IndexedDB and localStorage, allowing us to use the same API on both the server and the client, opening up some really interesting scenarios.
Today, there is a fully-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), which 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 nevertheless 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), which 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 us 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 = (pattern, listener) => { //[1] db.on('put', (key, val) => { //[2] const match = Object.keys(pattern).every( k => (pattern[k] === val[k]) //[3] ); if(match) { listener(key, val); //[4] } }); }; return db; };
That's it for our plugin, it's extremely simple. Let's briefly see what happens in the preceding code:
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:
const level = require('level'); //[1] const levelSubscribe = require('./levelSubscribe'); //[2] let db = level(__dirname + '/db', {valueEncoding: 'json'}); db = levelSubscribe(db); db.subscribe( {doctype: 'tweet', language: 'en'}, //[3] (k, val) => console.log(val) ); db.put('1', {doctype: 'tweet', text: 'Hi', language: 'en'}); //[4] 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'
.put
. The first call will trigger the callback associated to our subscription and we will see the stored object printed in the console. This is because in this case the object matches the subscription. Instead, the second call will not generate any output because the stored object will not match the subscription criteria.This example shows a real application of the decorator pattern in its simplest 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 us 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