Pseudoclassical Decorators

We’re now going to examine a variation of the Decorator first presented in a JavaScript form 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 will define in more detail shortly.

Note

This particular variation of the Decorator pattern is provided for reference purposes. If you find it overly complex, I recommend opting for one of the simpler implementations covered earlier.

Interfaces

PJDP describes the Decorator pattern as one 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.

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

So, why would we 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 objects 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 that constructor/object based on the methods it implements.

// Create interfaces using a pre-defined Interface 
// constructor that accepts an interface name and 
// skeleton methods to expose. 

// In our reminder example summary() and placeOrder()
// represent functionality the interface should
// support
var reminder = new Interface( "List", ["summary", "placeOrder"] );

var properties = {
  name: "Remember to buy the milk",
  date: "05/06/2016",
  actions:{
    summary: function (){
      return "Remember to buy the milk, we are almost out!";
   },
    placeOrder: function (){
      return "Ordering milk from your local grocery store";
    }
  }
};

// Now create a constructor implementing the above properties
// and methods

function Todo( config ){
  
  // State the methods we expect to be supported
  // as well as the Interface instance being checked
  // against

  Interface.ensureImplements( config.actions, reminder );

  this.name = config.name;
  this.methods = config.actions;

}

// Create a new instance of our Todo constructor

var todoItem = Todo( properties );

// Finally test to make sure these function correctly

console.log( todoItem.methods.summary() );
console.log( todoItem.methods.placeOrder() );

// Outputs:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store

In this example, Interface.ensureImplements provides strict functionality checking, and code for both this and the Interface constructor can be found here.

The biggest problem with interfaces is that, as there isn’t built-in support for them in JavaScript, there is a danger of attempting to emulate a feature of another language that may not be an ideal fit. Lightweight interfaces can be used without a great performance cost, however, and we will next look at Abstract Decorators using this same concept.

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 us to “decorate” our Macbook with a number of enhancements for an additional fee.

Enhancements can include upgrades to 4 GB or 8 GB of 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 would 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 as they 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(){
      // Base price
      return 900.00;         
    }
};

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, it calls 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 overridden as 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 we can see, most of this is relatively straightforward 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 have learned.

// Instantiation of the macbook
var myMacbookPro = new MacbookPro();  

// Outputs: 900.00
console.log( myMacbookPro.getPrice() );

// Decorate the macbook
myMacbookPro = new CaseDecorator( myMacbookPro );

// This will return 945.00
console.log( myMacbookPro.getPrice() );

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 for each object type. This makes maintaining applications that may require a large number of subclassed objects significantly more straightforward.

Get Learning JavaScript Design Patterns now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.