I. Simplest Decorator Implementation
JS has natural advantages in dynamic extension, very easy to implement decorators:
// Initial type
function Dog() {
console.log('I am a dog');
}
// Decorator types
function CanRun(dog) {
dog.run = function() {
console.log('I can run');
}
return dog;
}
function CanWalk(dog) {
dog.walk = function() {
console.log('I can walk');
}
return dog;
}
function CanBark(dog) {
dog.bark = function() {
console.log('I can bark');
}
return dog;
}
// ...
// test
var dog = new Dog();
CanWalk(CanBark(CanRun(dog))); // Extend dog's functionality through "wrapping"
dog.run();
dog.bark();
dog.walk();
Simple enough, but there are some problems:
- Seems like we don't need the Decorator Pattern, just put all functionality into
Dog, right?
From the above example it indeed seems so, but what if Dog is a third-party component we cannot directly modify? At this time extending functionality through Decorator Pattern is very suitable. From this perspective, Decorator Pattern and Facade Pattern are very similar, the only difference is the purpose, the former is for extending new functionality, the latter pursues ease of use of existing interfaces
- Which functionality should exist as decorator types?
Basic, necessary functionality should be part of Dog, optional, extra, infrequently used functionality should be provided by decorator types
- What if decorators accidentally overwrite existing properties?
There is indeed risk of properties being overwritten, because we haven't done any type constraints, each decorator is also relatively independent, and might overwrite properties added by other decorators, we need more reliable (introduced later) decorator implementation to avoid these risks
II. Pseudo-Classic Decorator
JS doesn't provide Interface support, we cannot constrain types through interfaces to improve their reliability, but we can implement Interface ourselves to constrain types, simple Interface might be like this:
function Interface(strName, arrMethodNames) {
this.name = strName;
this.strMethods = arrMethodNames;
}
Interface.ensureImplements = function(obj, interface) {
for(var i = 0; i < interface.strMethods.length; i++) {
if (!(interface.strMethods[i] in obj)) {
throw new TypeError('Interface.ensureImplements: no ' + interface.strMethods[i] + '\'s here');
}
}
}
Use custom Interface to implement type constraints, Decorator Pattern can become like this:
// Equivalent to decorator object
var spec = {
attr: 'value',
actions: {
fun1: function() {
console.log('fun1');
},
fun2: function() {
console.log('fun2');
}
}
}
var myInterface = new Interface('myInterface', ['fun1', 'fun2']);
// Constructor
function MyObject(spec) {
// Interface check
Interface.ensureImplements(spec.actions, myInterface);
this.attr = spec.attr;
this.methods = spec.actions;
}
// test
var obj = new MyObject(spec);
obj.methods.fun1();
obj.methods.fun2();
Although using interfaces implemented type constraints, structure is not clear enough, not convenient for management, most easy to manage is of course hierarchical structure, which is the inheritance mechanism in abstract decorators below
III. Abstract Decorator
Define optional functionality first in abstract decorator class, but don't provide implementation, let concrete decorator subclasses provide implementation, and use interfaces to implement type constraints, example code as follows:
// Define interface
var iCoffee = new Interface('coffee', ['addMilk', 'addSalt', 'addSugar']);
// Define base class
function Coffee() {
console.log('make a cup of coffee');
}
Coffee.prototype = {
addMilk: function() {},
addSalt: function() {},
addSugar: function() {},
getPrice: function() {
// Original flavor price
return 30;
}
}
// Define abstract decorator class
function CoffeeDecorator(coffee) {
Interface.ensureImplements(coffee, iCoffee);
this.coffee = coffee;
}
CoffeeDecorator.prototype = {
addMilk: function() {
return this.coffee.addMilk();
},
addSalt: function() {
return this.coffee.addSalt();
},
addSugar: function() {
return this.coffee.addSugar();
},
getPrice: function() {
return this.coffee.getPrice();
}
}
// Decorator subclasses
function MilkDecorator(coffee) {
// Call parent class constructor
this.superType(coffee);
}
// Define inheritance
function extend(subType, superType) {
var F = function() {};
F.prototype = superType.prototype;
subType.prototype = new F(); // Inherit prototype attributes
subType.prototype.superType = superType;
console.log(subType.prototype.superType);
}
extend(MilkDecorator, CoffeeDecorator); // Inheritance
// Override parent class methods (extension)
MilkDecorator.prototype.addMilk = function() {
console.log('add some milk');
}
MilkDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 8;
}
// ...Define other decorator subclasses
// test
var coffee = new Coffee();
console.log(coffee.getPrice()); // 30
coffee = new MilkDecorator(coffee);
console.log(coffee.getPrice()); // 38
This implementation method's advantage is clear structure, disadvantage is complexity increased, manually simulating features language itself doesn't provide may have potential risks, we simulated interfaces and inheritance mechanisms, may bury other hidden dangers
IV. Decorator Mechanism Provided by jQuery
Yes, again $.extend(), [Mixin Pattern_JavaScript Design Patterns 10](/articles/mixin 模式-javascript 设计模式 10/) said $.extend() provides Mixin Pattern implementation, here again says provides Decorator Pattern implementation, not conflicting, because these two patterns' purpose is both extending existing components' functionality, strictly speaking, jQuery's extend is more like Mixin Pattern (merge several components to produce new component, if saying this mechanism counts as Decorator Pattern, also barely passable)
For more information about jQuery's extend please see: jQuery.extend Function Detailed Explanation
V. Decorator Pattern Pros and Cons
Advantages
-
Separated basic functionality and optional functionality (decorator functionality)
-
Can dynamically extend object's functionality, without accidentally modifying basic object
Disadvantages
-
Introduced大量 small types, might cause namespace confusion
-
Improper management will make application structure more complex, especially implementation based on inheritance mechanism, deep inheritance will greatly reduce readability
References
- "JavaScript Design Patterns"
No comments yet. Be the first to share your thoughts.