JS设计模式——12.装饰者模式
装饰者模式概述
本章讨论的是一种为对象添加特性的技术,她并不使用创建新子类这种手段。
装饰者模式可以用来透明的把对象包装在具有同样接口的另一个对象中。这样一来,就可以给一个方法添加一些行为,然后将方法调用传递给原始对象。
装饰者的结构
装饰者可用于为对象添加功能,她可以用来替代大量子类。
我们还是来看那个自行车的例子(第7章),假设这件商店开始为每一种自行车提供一些额外的特色配件。现在顾客再加点钱就可以买到前灯、尾灯、前挂篮等。每一种可选配件都会影响到售价和车的组装方法。我们来用装饰者模式来解决这个问题。
在这个例子中,选件类就是装饰者,而自行车类是他们的组件。装饰者对其组件进行透明包装。
包装过程如下:
step 1: 修改接口,加入getPrice方法
var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair', 'getPrice']); var AcmeComfortCuiser = function(){ }; AcmeComfortCuiser.prototype = { assemble: function(){ }, wash: function(){ }, repair: function(){ }, getPrice: function(){ } }
step 2: 创建抽象类BicycleDecorator
var BicycleDecorator = function(bicycle){ Interface.ensureImplements(bicycle, Bicycle); this.bicycle = bicycle; }; BicycleDecorator.prototype = { assemble: function(){ return this.bicycle.assemble(); }, wash: function(){ return this.bicycle.wash(); }, repair: function(){ return this.bicycle.repair(); }, getPrice: function(){ return this.bicycle.getPrice(); } }
这个抽象类(装饰者类)的构造函数接受一个对象参数,并将其用作改装饰类的组件。
BicycleDecorator类是所有选件类的超类。对于那些不需要修改的方法,选件类只要从BicycleDecorator继承而来即可,而这些方法又会在组件上调用同样的方法,因此选件类对于任何客户代码都是透明的。
step 3: 创建选件类
var HeadlightDecorator = function(bicycle){ HeadlightDecorator.superclass.constructor.call(this, bicycle); }; extend(HeadlightDecorator, BicycleDecorator); HeadlightDecorator.prototype.getPrice = function(){ return this.bicycle.getPrice() + 15.00; }
这个类很简单,她重新定义了需要进行装饰的方法。
step 4: 使用选件类
var myBicycle = new AcmeComfortCuiser(); console.log(myBicycle.getPrice()); // 399.00 myBicycle = new HeadlightDecorator(myBicycle); console.log(myBicycle.getPrice()); // 414.00
这里用来存放那个HeadlightDecorator实例的不是另外一个变量,而是用来存放自行车的同一个变量。者意味着此后将不能访问原来的那个自行车对象,不过没关系,你以后不再需要这个对象。那个装饰者完全可以和自行车对象互换使用。这也意味着你可以随心所欲的嵌套多重装饰者。
var TaillightDecorator = function(bicycle){ TaillightDecorator.superclass.constructor.call(this, bicycle); }; extend(TaillightDecorator, BicycleDecorator); TaillightDecorator.prototype.getPrice = function(){ return this.bicycle.getPrice() + 9.00; } var myBicycle = new AcmeComfortCuiser(); console.log(myBicycle.getPrice()); // 399.00 myBicycle = new HeadlightDecorator(myBicycle); console.log(myBicycle.getPrice()); // 414.00 myBicycle = new TaillightDecorator(myBicycle); console.log(myBicycle.getPrice()); // 423.00
装饰者修改其组件的方式
装饰者的作用就在于以某种方式对其组件对象的行为进行修改。
在方法之后添加行为
上面提到的就是这种方法,在这里补充一下为何她能够嵌套多重装饰者。
其实那个就是一个栈。在TaillghtDecorator对象上调用getPrice方法,这将转至HeadlightDeocator上的getPrice方法,从而转至AcmeComfortcruiser对象上并返回其价格,一直到最外层上,最后的就过就是399+15+9=423.
在方法之前添加行为
var FrameColorDecorator = function(bicycle, frameColor){ FrameColorDecorator.superclass.constructor.call(this, bicycle); this.frameColor = frameColor; }; extend(FrameColorDecorator, BicycleDecorator); FrameColorDecorator.prototype.assemble = function(){ return 'Paint the frame '+this.frameColor+'and allow it to dry' + this.bicycle.assemble(); }
这里通过传递参数的方法实现了在方法之前添加行为。
替换方法
这个没什么好说的,就是在重写方法的时候不调用this.bicycle.method()或者在一定条件下才调用this.bicycle.method()
添加新方法
下面我们来添加一个新方法。
var BellDecorator = function(){ BellDecorator.superclass.constructor.call(this, bicycle); }; extend(BellDecorator, BicycleDecorator); BellDecorator.prototype.ringBell = function(){ return 'Bell rung'; };
因为我们并没有在组件类实现这个方法,所以只有当BellDecorator是最后一个被调用的时候才可以访问到ringBell方法,如下:
var myBicycle = new AcmeComfortCuiser(); console.log(myBicycle.getPrice()); // 399.00 myBicycle = new HeadlightDecorator(myBicycle); console.log(myBicycle.getPrice()); // 414.00 myBicycle = new BellDecorator(myBicycle); console.log(myBicycle.ringBell());//this is ok myBicycle = new TaillightDecorator(myBicycle); console.log(myBicycle.ringBell()); //this is not ok
这个问题有多个解决方案,比如在组件中定义此方法,或者设置一个过程,她可以确保如果使用了BellDecorator的话,那么他将最后被调用。但是第二种方法是有局限性的,如果我们添加了两个方法,岂不是解决不了了。
最好的方法是在BicycleDecorator的构造函数中添加一些代码,他们对组件对象进行检查,并为其拥有的每一个方法创建一个通道方法。这样以来,如果在BellDecorator外再裹上另外一个装饰者的话,内层装饰者定义的新方法仍然可以访问。具体代码如下:
var BicycleDecorator = function(bicycle){ this.bicycle = bicycle; this.interface = Bicycle; outerloop: for(var key in this.bicycle){ if(typeof this.bicycle[key] !== 'function'){ continue outerloop; } for(var i= 0, len=this.interface.methods.length; i<len; i++){ if(key === this.interface.methods[i]){ continue outerloop; } } var that = this; (function(methodName){ that[methodName] = function(){ return that.bicycle[methodName](); }; })(key); } };
工厂模式的作用
下面我们用工厂模式重新改进createBicycle方法,在这里工厂模式可以统揽各种类(包括自行车类也包括装饰者类)。
var AcmeBicycleShop = function(){ }; extend(AcmeBicycleShop, BicycleShop); AcmeBicycleShop.prototype.createBicycle = function(model, options){ var bicycle = new AcmeBicycleShop.models[model](); for(var i= 0, len=options.length; i<len; i++){ var decorator = AcmeBicycleShop.options[options[i].name]; if(typeof decorator !== 'function'){ throw new Error('Decorator '+options[i].name+' is not found'); } var argument = options[i].arg; bicycle = new decorator(bicycle, argument); } Interface.ensureImplements(bicycle, Bicycle); return bicycle; }; AcmeBicycleShop.models = { 'The Speedster': AcmeSpeedster, 'The Lowrider': AcmeLowrider }; AcmeBicycleShop.options = { 'headlight': HeadlightDecorator, 'taillight': TaillightDecorator, 'bell': BellDecorator, 'color': FrameColorDecorator };
这样一来,对象的实例化就简单多了。
函数装饰者
下面一个就创建了一个包装另外一个函数的装饰者,她的作用在于将被包装者的返回结果改为大写:
function upperCaseDecorator(func){ return func.apply(this, arguments).toUpperCase(); } function good(){ return 'Well Done!'; } console.log(upperCaseDecorator(good));
装饰者的使用场合
- 如果需要为类添加特性或职责,而从该类派生子类的解决办法并不实际的话,就应该使用装饰者模式。
- 如果需要为对象增添特性而又不想改变该对象的代码的话,也可以采用装饰者模式。
示例:性能分析器
我们打算在每个方法调用的前后添加一些代码,分别用于启动计时器和停止计时器并报告结果。这个装饰者必须完全透明,这样她才能应用于任何对象而又不干扰其正常的代码执行。
首先我们来创建一个测试类
var ListBuilder = function(parent, listLength){ this.parentEl = $(parent); this.listLength = listLength; }; listBuilder.prototype = { buildList: function(){ var list = document.createElement('ol'); this.parentEl.appendChild(list); for(var i=0; i<this.listLength; i++){ var item = document.createElement('li'); list.appendChild(item); } } }
下面创建装饰者
var SimpleProfiler = function(component){ this.component = component; }; SimpleProfiler.prototype = { buildList: function(){ var startTime = new Date(); this.component.buildList(); var elapsedTime = (new Date()).getTime() - startTime.getTime(); console.log('buildList: '+elapsedTime+'ms'); } } var list = new ListBuilder('list-container', 5000); list = new SimpleProfiler(list); list.buildList();
对她进行通用化改造
加入现在我又为上面的例子添加removeList的方法,那么这时应该如何做呢?我们最好的选择就是对其进行改造了。
var MethodProfiler = function(component){ this.component = component; this.times = {}; for(var key in this.component){ if(typeof this.component[key] !== 'function'){ continue; } } var that = this; (function(methodName){ that[methodName] = function(){ that.startTimer(methodName); var returnValue = that.component[methodName].apply(that.component, arguments); that.displayTime(methodName, that.getElapsedTime(methodName)); return returnValue; }; })(key); }; MethodProfiler.prototype = { startTimer: function(methodName){ this.timers[methodNmae] = (new Date()).getTime(); }, getElapsedTime: function(methodName){ return (new Date()).getTime() - this.times[methodName]; }, displayTime: function(methodName, time){ console.log(methodName + ': ' +time+'ms'); } } var list = new ListBuilder('list-container', 5000); list = new MethodProfiler(list); list.buildList('ol'); list.buildList('ul'); list.removeList('ol'); list.removeList('ul');
那个for...in循环逐一检查组件对象的每一个属性,跳过不是方法的属性,如果遇到方法属性,则为装饰者添加一个同命方法。这样添加的新方法中的代码会启动计时器、调用组件的同命方法、停止计时器以及返回先前保存下来的组件的同命方法的返回值。
装饰者模式之利
- 装饰者是在运行期间为对象添加特性或职责的有利工具。
- 装饰者的运作是透明的,这就是说我们可以用她包装其他对象,然后继续按之前使用那些对象的方法来使用她。
装饰者模式之弊
- 在遇到用装饰者包装起来的对象时,那些依赖于类型检查的代码会出问题。
- 使用装饰者模式往往会增加架构的复杂程度。