一.最简单的装饰者实现
JS在动态扩展方面有着天生的优势,很容易就能实现装饰者:
// 初始类型
function Dog() {
console.log('I am a dog');
}
// 装饰类型
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))); // 通过“包裹”扩展dog的功能
dog.run();
dog.bark();
dog.walk();
简单是足够简单了,但存在一些问题:
好像不需要用装饰者模式吧,直接把功能全放进
Dog
里不就好了吗?从上面的例子看确实是这样的,但如果Dog是我们无法直接修改的第三方组件呢,这时通过装饰者模式来扩展功能就很合适了。从这个角度看,装饰者模式和外观模式很相像,唯一的差别是目的不同,前者是为了扩展新功能,后者追求现有接口易用
哪些功能应该作为装饰类型存在?
基础的、必要的功能应该是
Dog
的组成部分,可选的、额外的、不常用的功能应该有装饰类型提供装饰者不小心重写了原有的属性怎么办?
确实存在属性被重写的风险,因为我们没有做任何类型上的约束,各个装饰者之间也是相对独立的,还有可能覆盖掉其他装饰者添上的属性,我们需要更可靠的(后文介绍的)装饰者实现来避免这些风险
二.伪经典装饰者
JS没有提供Interface支持,我们无法通过接口来约束类型,提高其可靠性,但我们可以自己实现Interface来约束类型,简单的Interface可能是这样的:
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');
}
}
}
利用自定义的Interface
来实现类型约束,装饰者模式可以变成这样:
// 作用相当于装饰对象
var spec = {
attr: 'value',
actions: {
fun1: function() {
console.log('fun1');
},
fun2: function() {
console.log('fun2');
}
}
}
var myInterface = new Interface('myInterface', ['fun1', 'fun2']);
// 构造函数
function MyObject(spec) {
// 接口检查
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();
虽然利用接口实现了类型约束,但结构不够清晰,不便于管理,最易于管理的当然是层级结构,也就是下面抽象装饰者中的继承机制
三.抽象装饰者
把可选功能先定义在抽象装饰者类中,但不提供实现,由具体装饰者子类提供实现,并利用接口实现类型约束,示例代码如下:
// 定义接口
var iCoffee = new Interface('coffee', ['addMilk', 'addSalt', 'addSugar']);
// 定义基类
function Coffee() {
console.log('make a cup of coffee');
}
Coffee.prototype = {
addMilk: function() {},
addSalt: function() {},
addSugar: function() {},
getPrice: function() {
// 原味价格
return 30;
}
}
// 定义抽象装饰者类
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();
}
}
// 装饰者子类
function MilkDecorator(coffee) {
// 调用父类构造函数
this.superType(coffee);
}
// 定义继承
function extend(subType, superType) {
var F = function() {};
F.prototype = superType.prototype;
subType.prototype = new F(); // 继承原型属性
subType.prototype.superType = superType;
console.log(subType.prototype.superType);
}
extend(MilkDecorator, CoffeeDecorator); // 继承
// 重写父类方法(扩展)
MilkDecorator.prototype.addMilk = function() {
console.log('add some milk');
}
MilkDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 8;
}
// ...定义其它装饰者子类
// test
var coffee = new Coffee();
console.log(coffee.getPrice()); // 30
coffee = new MilkDecorator(coffee);
console.log(coffee.getPrice()); // 38
这种实现方式的优点是结构清晰,缺点是复杂度增加了,手动模拟语言本身没有提供的特性可能有潜在的风险,我们模拟了接口和继承机制,可能埋下其它隐患
四.jQuery提供的装饰者机制
嗯,又是$.extend()
,Mixin模式_JavaScript设计模式10中说$.extend()
提供了Mixin模式的实现,这里又说提供了装饰者模式的实现,并不冲突,因为这两种模式的目的都是扩展现有组件的功能,严格地说,jQuery
的extend
更像Mixin模式(合并几个组件产生新组件,如果说这种机制算装饰者模式,也勉强说得过去)
关于jQuery
的extend
的更多信息请查看:jQuery.extend 函数详解
五.装饰者模式的优缺点
优点
分离了基础功能和可选功能(装饰功能)
可以动态扩展对象的功能,而不会意外修改基本对象
缺点
引入了大量小类型,可能会引起命名空间混乱
管理不当会使应用的结构更复杂,尤其是基于继承机制的实现,深层继承会极大地降低可读性
参考资料
- 《JavaScript设计模式》