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:
- It is applied to a whole class/trait prototype.
- 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:
- A member decorator can act as a class/trait decorator .
- 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);
}
});