The Decorator Pattern

Decorators are a structural design pattern that aim to promote code reuse. Similar to Mixins, they can be considered another viable alternative to object subclassing.

Classically, Decorators offered the ability to add behavior to existing classes in a system dynamically. The idea was that the decoration itself wasn’t essential to the base functionality of the class; otherwise, it would be baked into the superclass itself.

They can be used to modify existing systems where we wish to add additional features to objects without the need to heavily modify the underlying code using them. A common reason why developers use them is that their applications may contain features requiring a large quantity of distinct types of object. Imagine having to define hundreds of different object constructors for, say, a JavaScript game (Figure 9-11).

Decorator pattern

Figure 9-11. Decorator pattern

The object constructors could represent distinct player types, each with differing capabilities. A Lord of the Rings game could require constructors for Hobbit, Elf, Orc, Wizard, Mountain Giant, Stone Giant, and so on, but there could easily be hundreds of these. If we then factored in capabilities, imagine having to create subclasses for each combination of capability type—e.g., HobbitWithRing, HobbitWithSword, HobbitWithRingAndSword, and so on. This isn’t very practical and certainly isn’t manageable when we factor in a growing number of 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 relying on prototypal inheritance, we work with a single base object and progressively add decorator objects that provide the additional capabilities. The idea is that rather than subclassing, we add (decorate) properties or methods to a base object so it’s a little more streamlined.

Adding new attributes to objects in JavaScript is a very straightforward process, so with this in mind, a very simplistic decorator may be implemented as follows (Examples 9-7 and 9-8):

Example 9-7. Decorating Constructors with New Functionality

// A vehicle constructor
function vehicle( vehicleType ){

    // some sane 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 );

// Outputs:
// 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 );

// Outputs:
// vehicle:truck, model:CAT, color: blue

// Demonstrate "vehicle" is still unaltered
var secondInstance = new vehicle( "car" );
console.log( secondInstance );

// Outputs:
// vehicle: car, model:default, license: 00000-000

This type of simplistic implementation is functional, but it doesn’t really demonstrate all of the strengths Decorators have to offer. 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 modeled around a Macbook purchase.

Example 9-8. Decorating Objects with Multiple Decorators

// The constructor to decorate
function MacBook() { 

  this.cost = function () { return 997; }; 
  this.screenSize = function () { return 11.6; }; 

} 

// 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 );

// Outputs: 1522
console.log( mb.cost() );

// Outputs: 11.6
console.log( mb.screenSize() );

In the example, our Decorators are overriding the MacBook() superclass object’s .cost() function to return the current price of the Macbook plus the cost of the upgrade being specified.

It’s considered a decoration’ as the original Macbook objects’ constructor methods that are not overridden (e.g. screenSize()), as well as any other properties that we may define as a part of the Macbook, remain unchanged and intact.

There isn’t really a defined interface in the previous example, and we’re shifting away the responsibility of ensuring an object meets an interface when moving from the creator to the receiver.

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.