Reputation: 5038
I'm running through Addy Osmani's tutorial on The Decorator Pattern (found here http://addyosmani.com/blog/decorator-pattern/) and I'm a little confused on how to implement the most simplistic Decorator in Javascript. It seems that some examples use the obj.prototype pattern to add functionality to an existing object and some create a standalone function and pass an object.
// Decorator Pattern ?
function coffee(size, flavors) {
this._size = size || "medium";
this._flavors = flavors || [];
this._cost = 100;
this.info = function() {
console.log(this._size, this._flavors, this._cost);
}
}
// Decorator - Would this be considered a decorator since the functionality needed to add flavors default to the coffee object?
function addFlavor(coffee, flavor) {
coffee._flavors.push(flavor);
coffee._cost = coffee._cost + 25;
}
// Decorator - Engrave the cup? lol
function engraving(coffee) {
coffee._cost = coffee._cost + 200;
}
// Decorator Variation w/ prototype instead - Add cream
coffee.prototype.addCream = function () {
this._cost = this._cost + 100;
};
// Instantiate Coffee
testCoffee = new coffee('Large', ['vanilla']);
// Add Flavors
addFlavor(testCoffee, 'chocolate');
addFlavor(testCoffee, 'almond');
addFlavor(testCoffee, 'hazelnut');
// Add Engraving
engraving(testCoffee);
// Add Cream
testCoffee.addCream();
// Log it all to the console
testCoffee.info();
A JsFiddle of this example can be found here: http://jsfiddle.net/pathsofdesign/ocbbzoy2/
My question(s): It looks like I can implement the Decorator pattern using prototypal inheritance. Are there any pros or cons to doing it this way (Ie: my addCream() method)? Thanks!
UPDATE:
It looks like my example isn't actually implementing the Decorator pattern at all. Both @Etai and @Bergi gave great answers below. If I understand correctly, a traditional JS Decorator 'wraps' another obj and then makes adjustments to the behavior of that particular object without modifying the base object.
Upvotes: 3
Views: 2520
Reputation: 1493
I think that tutorial is quite confusing.
Say you want to create a simpleItem as your basic item, and a complexItem which is your simpleItem plus more.
Using prototypical inheritance:
function SimpleItem(name){
this.name = name;
}
function ComplexItem(size){
this.size = size;
}
ComplexItem.prototype = new SimpleItem('complex');
var item = new ComplexItem(3); //{size: 3, name: 'complex'}
ComplexItem.prototype.name = 'new complex name'; //item is now {size: 3, name: 'new complex name'}
Using the decorator pattern:
function SimpleItem(name){
this.name = name;
}
function ComplexItem(size){
SimpleItem.call(this, 'complex');
this.size = size;
}
var item = new ComplexItem(3); //{size: 3, name: 'complex'}
ComplexItem.prototype.name = 'new complex name'; //item is still {size: 3, name: 'complex'}
Although it looks like ComplexItem inherits from SimpleItem, it really doesn't. It's decorated by it. It's really the same as doing this:
function decorateMe(name){
this.name = name;
}
function ComplexItem(size){
decorateMe.call(this, 'complex');
this.size = size;
}
This allows something similar to 'multiple inheritance', but changing something in the parent later on does not affect the the descendants that have already been created.
In general, the decorator pattern means that you 'decorate' your instance by executing some code which will change it.
EDIT: Note that though I used this example by decorating properties, as @Bergi noted, the decorator is really used to decorate with behaviors (functionality). @Bergi's answer is actually the classical decorator pattern, which is to wrap an old method with a new one and thus 'decorate' it. My example is more of a mixin/extend pattern. However, the main idea with the pattern is that you are altering it in run-time and not actually inheriting from another prototype.
Upvotes: 3
Reputation: 664297
No. What you have there are not decorators, they're just methods. Methods do mutate the object, decorators mutate behaviour by overwriting methods. They're quite similar to mixins, only that they don't create new methods.
For example, let's give your Coffee
class a setSize
method:
Coffee.prototype.setSize = function(size) {
this._size = size || 'medium';
};
Now, let's have a crazy barista who doesn't get proportions right:
function extreme(coffee) {
var oldMethod = coffee.setSize;
coffee.setSize = function(size) {
oldMethod.call(this, size && 'Xtra'+size[0].toUpperCase()+size.slice(1));
};
}
and let him serve one that is ordered as "large":
> var coffee = extreme(new Coffee);
> coffee.setSize("large")
> coffee.info()
XtraLarge, Array [], 100
Upvotes: 7