Channels ▼
RSS

Web Development

Decorator Pattern in JavaScript


Similarly, we can implement other decorators, as many as needed. They can be extensions to the core Sale() functionality, implemented like plug-ins. They can even "live" in external files and be developed and shared by third-party developers:

Sale.decorators.quebec = {
  getPrice: function () {
    var price = this.uber.getPrice();
    price += price * 7.5 / 100;
    return price;
  }
};

Sale.decorators.money = {
  getPrice: function () {
    return "$" + this.uber.getPrice().toFixed(2);
  }
};

Sale.decorators.cdn = {
  getPrice: function () {
    return "CDN$ " + this.uber.getPrice().toFixed(2);
  }
};

Finally, let's see the "magic" method called decorate() that ties all the pieces together. Remember it will be called like this:

sale = sale.decorate('fedtax');

The 'fedtax' string will correspond to an object that's implemented in Sale.decorators.fedtax. The newly decorated object newobj will inherit the object we have so far (either the original, or the one after the last decorator has been added), which is the object this. To do the inheritance part, we'll use the temporary constructor pattern. We also set the uber property of newobj, so the children have access to the parent. Then we copy all the extra properties from the decorator to the newly decorated object newobj. At the end, newobj is returned and, in our example, it becomes the new updated sale object:

Sale.prototype.decorate = function (decorator) {
  var F = function () {},
    overrides = this.constructor.decorators[decorator],
    i, newobj;
  F.prototype = this;
  newobj = new F();
  newobj.uber = F.prototype;
  for (i in overrides) {
    if (overrides.hasOwnProperty(i)) {
      newobj[i] = overrides[i];
    }
  }
  return newobj;
};

Implementation Using a List

Let's explore a slightly different implementation, which benefits from the dynamic nature of JavaScript and doesn't need to use inheritance at all. Also, instead of having each decorated method call the method previously in the chain, we can simply pass the result of the previous method as a parameter to the next method.

Such implementation could also allow for easy undecorating or undoing a decoration, which means simply removing an item from the list of decorators.

The usage example will be slightly simpler because we don't assign the return value from decorate() to the object. In this implementation, decorate() doesn't do anything to the object, it simply appends to a list:

var sale = new Sale(100); // the price is 100 dollars
sale.decorate('fedtax');  // add federal tax
sale.decorate('quebec');  // add provincial tax
sale.decorate('money');   // format like money
sale.getPrice();          // "$112.88"

The Sale() constructor now has a list of decorators as an own property:

function Sale(price) {
  this.price = (price > 0) || 100;
  this.decorators_list = [];
}

The available decorators are once again implemented as properties of Sale.decorators. Note that the getPrice() methods are now simpler because they don't call the parent getPrice() to get the intermediate result; this result is passed to them as a parameter:

Sale.decorators = {};

Sale.decorators.fedtax = {
  getPrice: function (price) {
    return price + price * 5 / 100;
  }
};

Sale.decorators.quebec = {
  getPrice: function (price) {
    return price + price * 7.5 / 100;
  }
};

Sale.decorators.money = {
  getPrice: function (price) {
    return "$" + price.toFixed(2);
  }
};

The interesting part happens in the parent's decorate() and getPrice() methods. In the previous implementation, decorate() was somewhat complex and getPrice() was quite simple. In this implementation it's the other way around: decorate() just appends to a list, while getPrice() does all the work. The work includes going through the list of currently added decorators and calling each of the getPrice() methods, passing the result from the previous:

Sale.prototype.decorate = function (decorator) {
  this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
  var price = this.price,
    i,
    max = this.decorators_list.length,
    name;
  for (i = 0; i < max; i += 1) {
    name = this.decorators_list[i];
    price = Sale.decorators[name].getPrice(price);
  }
  return price;
};

This second implementation of the decorator pattern is simpler, and there's no inheritance involved. The decorating methods are also simpler. All the work is done by the method that "agrees" to be decorated. In this sample implementation, getPrice() is the only method that allows decoration. If you want to have more methods that can be decorated, then the part of going through the list of decorators should be repeated by each additional method. However, this can be easily abstracted into a helper method that takes a method and makes it "decoratable." In such an implementation the decorators_list property would become an object with properties named after the methods and values being arrays of decorator objects.


This article first appeared in JavaScript Patterns by the author, published by O'Reilly Media.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV