permalink

6

Exploring The Decorator Pattern In JavaScript & jQuery

Today we'll be taking a look at the decorator pattern, a structural pattern that promotes code reuse and is a flexible alternative to subclassing. This pattern is also useful for modifying existing systems where you may wish to add additional features to objects without the need to change the underlying code that uses them.

Traditionally, the decorator is defined as a design pattern that allows behaviour to be added to an existing object dynamically. The idea is that the decoration itself isn't essential to the base functionality of an object otherwise it would be baked into the 'superclass' object itself.

Subclassing

For developers unfamiliar with subclassing, here is a beginner's primer on them before we dive further into decorators: subclassing is a term that refers to inheriting properties for a new object from a base or 'superclass' object.

In traditional OOP, a class B is able to extend another class A. Here we consider A a superclass and B a subclass of A. As such, all instances of B inherit the methods from A. B is however still able to define it's own methods, including those that override methods originally defined by A.

Should B need to invoke a method in A that has been overriden, we refer to this as method chaining. Should B need to invoke the constructor A() (the superclass), we call this constructor chaining.

In order to demonstrate subclassing, we first need a base object that can have new instances of itself created. Let's model this around the concept of a person.

var subclassExample = subclassExample || {};
subclassExample = {
    Person: function( firstName , lastName ){
        this.firstName = firstName;
        this.lastName =  lastName;
        this.gender = 'male'
    } 
}

Next, we'll want to specify a new class (object) that's a subclass of the existing Person object. Let's imagine we want to add distinct properties to distinguish a Person from a Superhero whilst inheriting the properties of the Person 'superclass'. As superheroes share many common traits with normal people (eg. name, gender), this should hopefully illustrate how subclassing works adequately.

    
//a new instance of Person can then easily be created as follows:
var clark = new subclassExample.Person( "Clark" , "Kent" );
       
//Define a subclass constructor for for 'Superhero':
subclassExample.Superhero = function( firstName, lastName , powers ){
    /*
        Invoke the superclass constructor on the new object
        then use .call() to invoke the constructor as a method of
        the object to be initialized.
    */
    subclassExample.Person.call(this, firstName, lastName);
    //Finally, store their powers, a new array of traits not found in a normal 'Person'
    this.powers = powers;
}
subclassExample.Superhero.prototype = new subclassExample.Person;
var superman = new subclassExample.Superhero( "Clark" ,"Kent" , ['flight','heat-vision'] );
console.log(superman); /* includes superhero props as well as gender*/

The Superhero definition creates an object which descends from Person. Objects of this type have properties of the objects that are above it in the chain and if we had set default values in the Person object, Superhero is capable of overriding any inherited values with values specific to it's object.

So where do decorators come in?

Decorators

Decorators are used when it's necessary to delegate responsibilities to an object where it doesn't make sense to subclass it. A common reason for this is that the number of features required demand for a very large quantity of subclasses. Can you imagine having to define hundreds or thousands of subclasses for a project? It would likely become unmanagable fairly quickly.

To give you a visual example of where this is an issue, imagine needing to define new kinds of Superhero: SuperheroThatCanFly, SuperheroThatCanRunQuickly and SuperheroWithXRayVision.

Now, what if s superhero had more than one of these properties?. We'd need to define a subclass called SuperheroThatCanFlyAndRunQuickly , SuperheroThatCanFlyRunQuicklyAndHasXRayVision etc – effectively, one for each possible combination. As you can see, this isn't very manageable when you factor in different abilities.

The decorator pattern isn't heavily tied to how objects are created but instead focuses on the problem of extending their functionality. Rather than just using inheritance, where we're used to extending objects linearly, we work with a single base object and progressively add decorator objects which provide the additional capabilities. The idea is that rather than subclassing, we add (decorate) properties or methods to a base object so its a little more streamlined.

The extension of objects is something already built into JavaScript and as we know, objects can be extended rather easily with properties being included at any point. With this in mind, a very very simplistic decorator may be implemented as follows:

Example 1: Basic decoration of existing object constructors with new functionality

function vehicle( vehicleType ){
    /*properties and defaults*/
    this.vehicleType = vehicleType || 'car',
    this.model = 'default',
    this.license = '00000-000'
}
/*Test instance for a basic vehicle*/
var testInstance = new vehicle('car');
console.log(testInstance);
/*vehicle: car, model:default, license: 00000-000*/
/*Lets create a new instance of vehicle, to be decorated*/
var truck = new vehicle('truck');
/*New functionality we're decorating vehicle with*/
truck.setModel = function( modelName ){
    this.model = modelName;
}
truck.setColor = function( color ){
    this.color = color;
}
    
/*Test the value setters and value assignment works correctly*/
truck.setModel('CAT');
truck.setColor('blue');
console.log(truck);
/*vehicle:truck, model:CAT, color: blue*/
/*Demonstrate 'vehicle' is still unaltered*/
var secondInstance = new vehicle('car');
console.log(secondInstance);
/*as before, vehicle: car, model:default, license: 00000-000*/

This type of simplistic implementation is something you're likely familiar with, but it doesn't really demonstrate some of the other strengths of the pattern. For this, we're first going to go through my variation of the Coffee example from an excellent book called Head First Design Patterns by Freeman, Sierra and Bates, which is modelled around a Macbook purchase.

We're then going to look at psuedo-classical decorators.

Example 2: Simply decorate objects with multiple decorators

//What we're going to decorate
function MacBook() { 
    this.cost = function () { return 997; }; 
    this.screenSize = function () { return 13.3; }; 
} 
/*Decorator 1*/
function Memory(macbook) { 
    var v = macbook.cost(); 
    macbook.cost = function() { 
        return v + 75; 
    } 
} 
 /*Decorator 2*/
function Engraving( macbook ){
   var v = macbook.cost(); 
   macbook.cost = function(){
     return  v + 200;
  };
}
 
/*Decorator 3*/
function Insurance( macbook ){
   var v = macbook.cost(); 
   macbook.cost = function(){
     return  v + 250;
  };
}
var mb = new MacBook(); 
Memory(mb); 
Engraving(mb);
Insurance(mb);
console.log(mb.cost()); //1522
console.log(mb.screenSize()); //13.3

Here, the decorators are overrriding the superclass .cost() method to return the current price of the Macbook plus with the cost of the upgrade being specified. It's considered a decoration as the original Macbook object's constructor methods which are not overridden (eg. screenSize()) as well as any other properties which we may define as a part of the Macbook remain unchanged and in tact.

As you can probably tell, there isn't really a defined 'interface' in the above example and duck typing is used to shift the responsibility of ensuring an object meets an interface when moving from the creator to the receiver.

Pseudo-classical decorators

We're now going to examine the variation of the decorator presented in 'Pro JavaScript Design Patterns' (PJDP) by Dustin Diaz and Ross Harmes.

Unlike some of the examples from earlier, Diaz and Harmes stick more closely to how decorators are implemented in other programming languages (such as Java or C++) using the concept of an 'interface', which we'll define in more detail shortly.

Note: This particular variation of the decorator pattern is provided for reference purposes. If you find it overly complex for your application's needs, I recommend sticking to one the simplier implementations covered earlier, but I would still read the section. If you haven't yet grasped how decorators are different from subclassing, it may help!.

Interfaces

PJDP describes the decorator as a pattern that is used to transparently wrap objects inside other objects of the same interface. An interface is a way of defining the methods an object *should* have, however, it doesn't actually directly specify how those methods should be implemented.

They can also indicate what parameters the methods take, but this is considered optional.

So, why would you use an interface in JavaScript? The idea is that they're self-documenting and promote reusability. In theory, interfaces also make code more stable by ensuring changes to them must also be made to the classes implementing them.

Below is an example of an implementation of Interfaces in JavaScript using duck-typing – an approach that helps determine whether an object is an instance of constructor/object based on the methods it implements.

var TodoList = new Interface('Composite', ['add', 'remove']);
var TodoItem = new Interface('TodoItem', ['save']);
// TodoList class
var myTodoList = function(id, method, action) { 
        // implements TodoList, TodoItem
...
};
...
function addTodo(todoInstance) {
        Interface.ensureImplements(todoInstance, TodoList, TodoItem);
        // This function will throw an error if a required method is not implemented,
        // halting execution of the function.
        //...
}

where Interface.ensureImplements provides strict checking. If you would like to explore interfaces further, I recommend looking at Chapter 2 of Pro JavaScript design patterns. For the Interface class used above, see here.

The biggest problem with interfaces is that, as there isn't built-in support for them in JavaScript, there's a danger of us attempting to emulate the functionality of another language, however, we're going to continue demonstrating their use just to give you a complete view of how the decorator is implemented by other developers.

This variation of decorators and abstract decorators

To demonstrate the structure of this version of the decorator pattern, we're going to imagine we have a superclass that models a macbook once again and a store that allows you to 'decorate' your macbook with a number of enhancements for an additional fee.

Enhancements can include upgrades to 4GB or 8GB Ram, engraving, Parallels or a case. Now if we were to model this using an individual subclass for each combination of enhancement options, it might look something like this:

var Macbook = function(){
        //...
}
var MacbookWith4GBRam =  function(){},
       MacbookWith8GBRam = function(){},
       MacbookWith4GBRamAndEngraving = function(){},
       MacbookWith8GBRamAndEngraving = function(){},
       MacbookWith8GBRamAndParallels = function(){},
       MacbookWith4GBRamAndParallels = function(){},
       MacbookWith8GBRamAndParallelsAndCase = function(){},
       MacbookWith4GBRamAndParallelsAndCase = function(){},
       MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
       MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};

and so on.

This would be an impractical solution as a new subclass would be required for every possible combination of enhancements that are available. As we'd prefer to keep things simple without maintaining a large set of subclasses, let's look at how decorators may be used to solve this problem better.

Rather than requiring all of the combinations we saw earlier, we should simply have to create five new decorator classes. Methods that are called on these enhancement classes would be passed on to our Macbook class.

In our next example, decorators transparently wrap around their components and can interestingly be interchanged astray use the same interface.

Here's the interface we're going to define for the Macbook:

var Macbook = new Interface('Macbook', ['addEngraving', 'addParallels', 'add4GBRam', 'add8GBRam', 'addCase']);
A Macbook Pro might thus be represented as follows:
var MacbookPro = function(){
    //implements Macbook
}
MacbookPro.prototype = {
        addEngraving: function(){
        },
        addParallels: function(){
        },
        add4GBRam: function(){
        },
        add8GBRam:function(){
        },
        addCase: function(){
        },
        getPrice: function(){
                return 900.00; //base price.        
        }
};

We're not going to worry about the actual implementation at this point as we'll shortly be passing on all method calls that are made on them.

To make it easier for us to add as many more options as needed later on, an abstract decorator class is defined with default methods required to implement the Macbook interface, which the rest of the options will subclass.

Abstract decorators ensure that we can decorate a base class independently with as many decorators as needed in different combinations (remember the example earlier?) without needing to derive a class for every possible combination.

//Macbook decorator abstract decorator class
var MacbookDecorator = function( macbook ){
    Interface.ensureImplements(macbook, Macbook);
    this.macbook = macbook;    
}
MacbookDecorator.prototype = {
        addEngraving: function(){
            return this.macbook.addEngraving();
        },
        addParallels: function(){
            return this.macbook.addParallels();
        },
        add4GBRam: function(){
            return this.macbook.add4GBRam();
        },
        add8GBRam:function(){
            return this.macbook.add8GBRam();
        },
        addCase: function(){
            return this.macbook.addCase();
        },
        getPrice: function(){
            return this.macbook.getPrice(); 
        }        
};

What's happening in the above sample is that the Macbook decorator is taking an object to use as the component. It's using the Macbook interface we defined earlier and for each method is just calling the same method on the component. We can now create our option classes just by using the Macbook decorator – simply call the superclass constructor and any methods can be overriden as per necessary.

var CaseDecorator = function( macbook ){
    /*call the superclass's constructor next*/
    this.superclass.constructor(macbook);    
}
/*Let's now extend the superclass*/
extend( CaseDecorator, MacbookDecorator ); 
CaseDecorator.prototype.addCase = function(){
    return this.macbook.addCase() + " Adding case to macbook ";   
};
CaseDecorator.prototype.getPrice = function(){
    return this.macbook.getPrice() + 45.00;  
};

As you can see, most of this is relatively easy to implement. What we're doing is overriding the addCase() and getPrice() methods that need to be decorated and we're achieving this by first executing the component's method and then adding to it.

As there's been quite a lot of information presented in this section so far, let's try to bring it all together in a single example that will hopefully highlight what we've learned.

//Instantiation of the macbook
var myMacbookPro = new MacbookPro();  
//This will return 900.00
console.log(myMacbookPro.getPrice());
//Decorate the macbook
myMacbookPro = new CaseDecorator( myMacbookPro ); /*note*/
//This will return 945.00
console.log(myMacbookPro.getPrice());

An important note from PJDP is that in the line denoted *note*, Harmes and Diaz claim that it's important not to create a separate variable to store the instance of your decorators, opting for the same variable instead. The downside to this is that we're unable to access the original macbook object in our example, however we technically shouldn't need to further.

As decorators are able to modify objects dynamically, they're a perfect pattern for changing existing systems. Occasionally, it's just simpler to create decorators around an object versus the trouble of maintaining individual subclasses. This makes maintaining applications of this type significantly more straight-forward.

Implementing decorators with jQuery

As with other patterns I''ve covered, there are also examples of the decorator pattern that can be implemented with jQuery. jQuery.extend() allows you to extend (or merge) two or more objects (and their properties) together into a single object either at run-time or dynamically at a later point.

In this scenario, a target object can be decorated with new functionality without necessarily breaking or overriding existing methods in the source/superclass object (although this can be done).

In the following example, we define three objects: defaults, options and settings. The aim of the task is to decorate the 'defaults' object with additional functionality found in 'options', which we'll make available through 'settings'. We must:

(a) Leave 'defaults' in an untouched state where we don't lose the ability to access the properties or functions found in it a later point (b) Gain the ability to use the decorated properties and functions found in 'options'

var decoratorApp = decoratorApp || {};
/* define the objects we're going to use*/
decoratorApp = {
    defaults:{
              validate: false, 
              limit: 5, 
              name: "foo",
              welcome: function(){
                  //console.log('welcome!');
              }
             },
    options:{
             validate: true, 
             name: "bar", 
             helloWorld: function(){ 
                 //console.log('hello');
             }
            },
    settings:{},
    printObj: function(obj) {
            var arr = [];
            $.each(obj, function(key, val) {
            var next = key + ": ";
            next += $.isPlainObject(val) ? printObj(val) : val;
            arr.push( next );
      });
      return "{ " +  arr.join(", ") + " }";
    }
    
}
/* merge defaults and options, without modifying defaults */
decoratorApp.settings = $.extend({}, decoratorApp.defaults,decoratorApp.options);
/* what we've done here is decorated defaults in a way that provides access to the properties and functionality it has to offer (as well as that of the decorator 'options'). defaults itself is left unchanged*/
$('#log').append("<div><b>settings -- </b>" + decoratorApp.printObj(decoratorApp.settings) + "</div><div><b>options -- </b>" + decoratorApp. printObj(decoratorApp.options) + "</div><div><b>defaults -- </b>" +decoratorApp.printObj(decoratorApp.defaults) + "</div>" );
/*
settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log('welcome!'); }, helloWorld: function (){ console.log('hello!'); } }
options -- { validate: true, name: bar, helloWorld: function (){ console.log('hello!'); } }
defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log('welcome!'); } }
*/

Pros and cons of the pattern

Developers enjoy using this pattern as it can be used transparently and is also fairly flexible – as we've seen, objects can be wrapped or 'decorated' with new behavior and then continue to be used without needing to worry about the base object being modified. In a broader context, this pattern also avoids us needing to rely on large numbers of subclasses to get the same benefits.

There are however drawbacks that you should be aware of when implementing the pattern. If poorly managed, it can significantly complicate your application's architecture as it introduces many small, but similar objects into your namespace. The concern here is that in addition to becoming hard to manage, other developers unfamiliar with the pattern may have a hard time grasping why it's being used.

Sufficient commenting or pattern research should assist with the latter, however as long as you keep a handle on how widespread you use the decorator in your application you should be fine on both counts.

Conclusions

And that's it!. As with any design pattern, be sure that if you do employ the decorator when writing applications that you're using it for the benefits it offers rather than just using the pattern for the sake of it. I hope this post comes in useful!.

References:

6 Comments

  1. Thanks Alan (and Lon) – I appreciate your input. I'll take all of these into consideration while re-writing the second example. It'll hopefully be updated in the post shortly!

Leave a Reply

Required fields are marked *.