Decorator

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:

Decorator

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.

Techniques for implementing Decorators

Although Proxy and Decorator are conceptually two different patterns with different intents, they practically share the same implementation strategies. Let's review them.

Composition

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 augmentation

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!

Decorating a LevelUP database

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.

Introducing LevelUP and LevelDB

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!

Note

Find out more about the LevelUP ecosystem at: https://github.com/rvagg/node-levelup/wiki/Modules.

Implementing a LevelUP plugin

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.

Note

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:

  1. We decorated the db object with a new method named subscribe(). We simply attached the method directly to the provided db instance (object augmentation).
  2. We listened for any put operation performed on the database.
  3. We performed a very simple pattern-matching algorithm, which verified that all the properties in the provided pattern are also available on the data being inserted.
  4. If we have a match, we notify the listener.

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:

  1. First, we initialize our LevelUP database, choosing the directory where the files will be stored and the default encoding for the values.
  2. Then, we attach our plugin, which decorates the original db object.
  3. At this point, we are ready to use the new feature provided by our plugin, the subscribe() method, where we specify that we are interested in all the objects with doctype: 'tweet' and language: 'en'.
  4. Finally, we save some values in the database using 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.

Tip

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).

In the wild

For more examples of how Decorator is used in the real world, we might want to inspect the code of some more LevelUP plugins:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset