Decorators - nodirt/defineClass GitHub Wiki

Decorators

Decorators, borrowed from Python, allow changing behavior of classes and/or members by applying arbitrary functions to them.

Member decorators

A decorator is a function with two parameters: the member to decorate and its metadata. The decorator must return the decorated member.

The following example of a decorator implements function call logging:

function logging(func, info) {
  return function() {
    console.log("Entering " + info.name);
    try {
      return func.apply(this, arguments);
    } finally {
      console.log("Exiting " + info.name);
    }
  };
}

To apply a decorator to a member put it to a prototype field named <memberName>$:

var Phone = defineClass({
  _super: Device,

  dial$: logging, // apply "logging" to "dial"
  dial: function (number) {
    console.log("Dialing to ", number);
  }
});

This class definition is equivalent to

var Phone = defineClass({
  _super: Device,

  dial: logging(function (number) {
    console.log("Dialing to ", number);
  })
});

As a result, the dial method body is wrapped with logging:

var phone = new Phone();
phone.dial("120-0000");
// Output:
//   Entering dial
//   Dialing to 120-0000
//   Exiting dial

Decorators are applied after a class is constructed, including trait application.

Multiple decorators

You can apply as many decorators as you want:

// define one more decorator
function logErrors(func, info) {
  return function () {
    try {
      return func.apply(this, arguments);
    } catch (err) {
      console.log("Error occurred:", err);
      throw err;
    }
  };
}

var Phone = defineClass({
  _super: Device,

  // apply two decorators
  dial$: [logging, logErrors],
  dial: function (number) {
    console.log("Dialing to ", number);
  }
});

Which is equivalent to:

var Phone = defineClass({
  _super: Device,

  dial: logErrors(logging(function (number) {
    console.log("Dialing to ", number);
  }))
});

Decorator order matters.

Syntax considerations

Python uses the following syntax for decorators:

@logging
def dial(number):
  print("Dialing to", number)

This syntax cannot be implemented in defineClass because we are limited with JavaScript syntax.

However, there are advantage of the member$ syntax: you can apply a decorator to a method in a base class without overriding it:

var PhoneWithLogging = defineClass({
  _super: Phone,
  dial$: logging
});

Here the logging decorator is be applied to the Phone.prototype.dial method and the decorated implementation is put to PhoneWithLogging.prototype.dial.

Class/trait decorators

A class decorator is like a member decorator except:

  1. It is applied to a whole class/trait prototype.
  2. To apply a decorator put it into $ prototype field.

Traits are decorators

Since a trait is a function it can be used as a class decorator. The difference between putting a trait in _super and in $ is that, as a decorator, the trait is applied after the class is constructed:

// define a trait that will be used as a decorator
var EncryptedMessageSender = defineClass.trait({

  // fake encryption. In fact, just a char code incrementation
  encrypt: function (text) {
    var s = "",
        i;
    for (i = 0; i < text.length; i++) {
      s += String.fromCharCode(text.charCodeAt(i) + 1);
    }
    return s;
  },
  sendMessage: function (text) {
    this._super(this.encrypt(text));
  }
});

// decorate a class
var Phone = defineClass({
  _super: Device,
  // specify a class decorator
  $: EncryptedMessageSender,

  sendMessage: function (text) {
    console.log("Sending SMS:");
    console.log(text);
  }
});

Here the EncryptedMessageSender.prototype.sendMessage overrides Phone.prototype.sendMessage and so the resulting sendMessage encrypts the text parameter.

Let's check it:

var phone = new Phone(1000);
phone.sendMessage("Anna");
// Output:
//   Sending SMS
//   Boob

Decorators in traits

Decorators can be used in traits as well as in classes.

defineClass.decorator

defineClass.decorator utility function takes a member decorator and enhances it with the following features:

  1. A member decorator can act as a class/trait decorator .
  2. Method opt of a decorator allows specifying decorator options.

Usage

var logging = defineClass.decorator(function (func, info) {
  return function() {
    console.log("Entering " + info.name);
    try {
      return func.apply(this, arguments);
    } finally {
      console.log("Exiting " + info.name);
    }
  };
});

Now it can be applied to a whole class:

var Phone = defineClass({
  _super: Device,
  $: logging,

  sendMessage: function (text) {
    console.log("Sending SMS:");
    console.log(text);
  }
});

Now both sendMessage method and turnOn method in the Device class are decorated.

Options

When defined with defineClass.decorator, a member decorator receives a third parameter, which is decorator options.

var logging = defineClass.decorator(function (func, info, opt) { // the 3rd parameter is "opt"
  // add a timestamp if "showTime" option is true
  function write(text) {
    if (opt.showTime) {
      text = new Date() + ": " + text;
    }
    console.log(text);
  }

  return function() {
    write("Entering " + info.name);
    try {
      return func.apply(this, arguments);
    } finally {
      write("Exiting " + info.name);
    }
  };
});

Use opt method of a decorator to specify options. The method receives new options and returns a new instance of the decorator:

var Phone = defineClass({
  _super: Device,
  
  // specify options and decorate the whole class
  $: logging.opt({ showTime: true }),

  sendMessage: function (text) {
    console.log("Sending SMS:");
    console.log(text);
  }
});