首页 > 代码库 > 5分钟读书笔记之 - 设计模式 - 装饰者模式
5分钟读书笔记之 - 设计模式 - 装饰者模式
本章讨论的是一种为对象增添特性的技术,它并不使用创建新子类这种手段。
装饰者模式可以透明地把对象包装在具有同样接口的另一对象之中,这样一来,你可以给一些方法添加一些行为,然后将方法调用传递给原始对象。相对于创建子类来说,使用装饰者模式对象是一种更灵活的选择。
装饰者可用于为对象增加功能。它可以用来替代大量子类。
考虑前面的自行车类,你现在可能提供一些配件供用户选择,装饰者模式要求我们只需要创建选件类,这些类与四种自行车类都要实现Bicycle接口,但是他们只被用作这些自行车类的包装类。在这个例子中,选件类就是装饰者,而自行车类是它的组件。装饰者对其组件进行了透明包装,二者可以互换使用。这是因为他们使用了同样的接口。下面我们来看应该怎样实现自行车装饰者类。
var Bicycle = new Interface(‘Bicycle‘,[‘assemble‘,‘wash‘,‘ride‘,‘repair‘,‘getPrice‘])
所有自行车类和选件装饰者都要实现这个接口,AcmeComfortCruiser 类大致就是这个样子
// The AcmeComfortCruiser classvar AcmeComfortCruiser = function(){};AcmeComfortCruiser.prototype = { assemble:function(){ ... }, wash:function(){ ... }, ride:function(){ ... }, repair:function(){ ... }, getPrice:function(){ return 399.00; }}
为了简化任务,也为了方便以后添加更多选择,我们将创建一个抽象类 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(); }, ride:function(){ return this.bicycle.ride(); }, repair:function(){ return this.bicycle.repair(); }, getPrice:function(){ return this.bicycle.getPrice(); }}
几乎没有比这更简单的装饰者模式类了,它的构造函数接受一个对象参数,并将其用作该装饰者的组件。该类实现了bicycle接口,它所实现的每一个方法所做的只是在其组件上调用同名方法。对于那些不需要修改的方法,选件类只要使用从BicycleDecorator继承而来的版本即可,而这些方法又会在组件上调用同样的方法,因此选件类对于任何客户代码都是透明的。
有了BicycleDecorator,创建各种选件类就很容易了。
var HeadlightDecorator = function(bicycle){ HeadlightDecorator.superclass.constructor.call(this,bicycle);}extend(HeadlightDecorator,BicycleDecorator);HeadlightDecorator.prototype.assemble = function(){ return this.bicyle.assemble()+‘ Attch headlight to handlebars.‘;}HeadlightDecorator.prototype.getPrice = function(){ return this.bicyle.getPrice()+15.00;}
这个类很简单,它重新定义了需要进行装饰的俩个方法。本例中装饰这些方法的做法是,先执行组件的方法,然后在此基础上附加一些装饰元素。assemble方法中是附加一条指示,getPrice方法则是把前灯的价格计入总价。
使用方法:
var myBicycle = new AcmeComfortCruiser();alert(myBicycle.getPrice());myBicycle = new HeadlightDecorator(myBicycle);alert(myBicycle.getPrice());
上面的第三行代码最为关键。这里用来存放那个HeadlightDecorator实例的不是另一个变量,而是用来存放自行车实例的同一变量。这意味着以后不能访问原来的那个自行车对象。这样意味着你可以随心所欲的嵌套使用多种装饰者,假如你创建一个 TaillightDecorator 类,那么可以将其与HeadlightDecorator结合使用。
var TaillightDecorator = function(bicycle){ TaillightDecorator.superclass.constructor.call(this,bicycle);}extend(TaillightDecorator,BicycleDecorator);TaillightDecorator.prototype.assemble = function(){ return this.bicyle.assemble()+‘ Attch taillight to the seat post.‘;}TaillightDecorator.prototype.getPrice = function(){ return this.bicyle.getPrice()+9.00;}var myBicycle = new AcmeComfortCruiser();alert(myBicycle.getPrice()); //399.00myBicycle = new TaillightDecorator(myBicycle);alert(myBicycle.getPrice()); //408.00myBicycle = new HeadlightDecorator(myBicycle);alert(myBicycle.getPrice()); //423.00
你可以如法炮制,创建无数个装饰者。通过在运行期间动态应用装饰者,你可以创建出具有所有需要特性的对象,而不用去维护那100个不同的子类。要是前灯的价格发生变化,你只需要在HeadlightDecorator类这个地方更新即可,维护工作则容易的多。
装饰者模式颇多得益于接口的使用,因此不必对代码进行修改,这是通过确保所有装饰者对象都实现了Bicycle接口而达到的。接口在此发挥着俩个方面的作用,首先它说明了装饰者必须实现哪些方法,其次它还可以在新版工厂方法中用来确保所创建对象都实现了必须的方法。
如果装饰者对象与其组件不能互换使用,它就丧失了其功用。这是装饰者模式的关键特点。
装饰者模式和组合模式有许多共同点和区别:
- 装饰者对象和组合对象都是用来包装别的对象。组合模式中称其为子对象,装饰者模式中称其为组件对象。他们都与所包装的对象实现同样的接口并且会把任何方法调用传递给这些对象。
- 组合模式是一种结构型的模式,用于把众多子对象组织为一个整体。藉此程序员与大批对象打交道时可以将他们当做一个对象来对待,并将它们组织为层次性的树。通常它并不修改方法调用,而只是将沿组合对象与子对象的链向下传递,直到到达并落实在叶对象上。
- 装饰者模式也是一种结构模式,它并非用于组织对象,而是用于在不修改现有对象或从其派生子类的前提下为其增填职责。在一些简单例子中,装饰者会透明而不加修改地传递所有调用方法,不过,创建装饰者的目的就是在于对方法进行修改。
- 尽管简单的组合对象可等同简单的装饰者,这二者却有着不同的焦点,组合对象并不修改方法调用,其着眼点在于组织子对象。而装饰者存在唯一目的就是修改方法调用而不是组织子对象,因为子对象只有一个,虽然这俩种结构相似,但是任务不同。
装饰者修改其组件的方式:
1.在方法之后添加行为。
在方法之后添加行为是最常见的修改方法的做法。具体而言就是先调用组件的方法,并在其返回后实施一些附加行为。例子:
TaillightDecorator.prototype.getPrice = function(){ return this.bicyle.getPrice()+9.00;}
这个例子中,首先对组件调用getPrice,然后再把前灯的价钱加在该方法调用所返回的价钱上,最后将其结果作为总价返回。这一过程可以根据需要多次重复,作为示范,下面将创建一辆带着俩个前灯和一个尾灯的自行车。
var myBicycle = new AcmeComfortCruiser();alert(myBicycle.getPrice()); //399.00myBicycle = new HeadlightDecorator(myBicycle);myBicycle = new HeadlightDecorator(myBicycle);myBicycle = new TaillightDecorator(myBicycle);alert(myBicycle.getPrice()); //438.00
这是修改组件最常见的做法,它在保留原有行为的基础上添加一些额外的行为后修改返回结果。
2.在方法之前添加行为
如果行为修改发生在执行组件之前,那么要么必须把装饰者安排在调用组件方法之前,要么必须设法修改传递给组件方法的参数值,下面的例子实现了一个提供车架颜色选择的装饰者。
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();}FrameColorDecorator.prototype.getPrice = function(){ return this.bicycle.getPrice()+30.00;}var myBicycle = new AcmeComfortCruiser();//把装饰者行为安排在调用组件方法之前。myBicycle = new FrameColorDecorator(myBicycle,‘red‘);myBicycle = new HeadlightDecorator(myBicycle);myBicycle = new TaillightDecorator(myBicycle);alert(myBicycle.getPrice()); //438.00
这里有俩点与以前的装饰者不同。首先该装饰者多了一个frameColor这个新状态,第二点差别在于本例assemble方法添加的步骤出现在其它组装指示之前而不是之后。装饰者并非只能在组件方式调用之后修改或者执行代码,相反,它也可以在调用组件之前执行代码。
3.替换方法
有时候为了实现新行为必须对方法进行整体替换,在此情况下,组件方法不会被调用(或者虽然被调用但其返回值会被抛弃)。作为修改的一个例子,下面我们创建一个用来实现自行车终身保修的装饰者。
var LifetimeWarrantyDecorator = function(bicycle){ LifetimeWarrantyDecorator.superclass.constructor.call(this, bicycle);}extend(LifetimeWarrantyDecorator, BicycleDecorator);LifetimeWarrantyDecorator.prototype.repair = function () { return ‘this bicycle is covered a lifetime....‘};LifetimeWarrantyDecorator.prototype.getPrice = function () { return this.bicycle.getPrice()+199.00;};
这个装饰者把repair方法替换为一个新方法。而组件的方法则再也不会被调用,装饰者也可以根据某种条件决定是否替换组件方法,在条件满足的时候替换方法,否则就使用组件方法。
在此之前的那些装饰者的应用顺序并不重要,但是,他们都必须放在最后应用。或者至少要放在所有其他修改repair方法的装饰者之后应用。
4.添加新方法
var BellDecorator = function(bicycle){ BellDecorator.superclass.constructor.call(this,bicycle);}extend(BellDecorator,BicycleDecorator);BellDecorator.prototype.assemble = function(){ return this.bicycle.assemble()+"attch bell!";}BellDecorator.prototype.getPrice = function(){ return this.bicycle.getPrice()+6.00;}BellDecorator.prototype.ringBell = function(){ return "Bell rung";}
这与前面讲过的装饰者非常相似,差别就是它实现了ringBell这个方法,但这个方法没有出现在接口中。
BellDecorator 必须放到最后使用,否则这个新方法将无法访问。这是因为其他装饰者只能传递他们知道的方法,也就是那些定义在接口中的方法,所以由于其他装饰者不知道ringBell方法,如果在添加铃铛之前调用的话,那么BellDecorator重新定义的方法就会被掩盖。这个问题有几个解决办法,你可以在接口中添加ringBell方法,并在BicycleDecorator超类中实现它,这样一来外层的装饰者就会传递这个方法。另外一个解决办法就是用一个设置过程来创建装饰者,它可以确保如果使用了BellDecorator对象的话,这个对象一定是处于最外层装饰者。作为一个临时解决方案还不错,但是如果还有另外的装饰者实现了新方法的话,就不太管用了。
最好的解决方法是在BicycleDecorator的构造函数中添加一些代码,他们对组件对象进行检查,并为其拥有的每一个方法创建通道方法。这样一来,如果在BellDecorator外再裹一个装饰者的话,内层装饰者定义的新方法仍可以访问。下面就是解决方案。
var BicycleDecorator = function (bicyle) { this.bicycle = bicyle; 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 _this = this; (function (methodName) { _this[methodName] = function () { return _this.bicycle[methodName](); }; })(key); }};BicycleDecorator.prototype = { assemble:function(){ return this.bicycle.assemble(); }, wash:function(){ return this.bicycle.wash(); }, ride:function(){ return this.bicycle.ride(); }, repair:function(){ return this.bicycle.repair(); }, getPrice:function(){ return this.bicycle.getPrice(); }}
在这个例子中,接口中的方法如通常一样定义在BicycleDecorator和prototype中,BicycleDecorator构造函数对组件对象进行检查,并为所找到每一个未见于接口中的方法创建一个新的通道方法,这样一来,外层的装饰者就不会掩盖内层装饰者定义的新方法,你就可以自由自在的创建各种实现新方法的装饰者了。
工厂的角色
装饰者的使用顺序有时候很重要,在理想情况下,装饰者应该能够以一种完全与顺序无关的方式创建,如果必须确保顺序,我们可以使用工厂对象。下面将重写AcmeBicycleShop类和createBicycle方法,以便用户可以指定自行车要配的选件,这些选件将被转化为装饰者,并在方法返回之前应用到新创建的自行车对象上。
原来的AcmeBicycleShop类如下:
var AcmeBicycleShop = function(){};extend(AcmeBicycleShop,BicycleShop);AcmeBicycleShop.prototype.createBicycle = function(model){ var bicycle; switch(model){ case ‘The Speedster‘: bicycle = new AcmSpeedster(); break; case ‘The Lowrider‘: bicycle = new AcmLowrider(); break; case ‘The Comfort Cruiser‘: default: bicycle = new AcmComfortCruiser(); } Interface.ensureImplements(bicycle,Bicycle); return bicycle;}
这个类的改进版允许用户指定想要的自行车配的选件,在这里,工厂模式可以统揽各种类,把所有的这些信息保存在一个地方,用户就可以把实际的类名与客户代码隔离开,这样以后添加新类或修改现有类也更容易,下面试改进版的代码:
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+‘not found.‘); } var argument = options[i].arg; bicycle = new decorator(bicycle, argument); } Interface.ensureImplements(bicycle,Bicycle); return bicycle;}AcmeBicycleShop.models = { ‘The Speedster‘:AcmSpeedster, ‘The Lowrider‘: AcmLowrider, ‘The Flatlander‘:AcmFlatlander, ‘The Comfort Cruiser‘:AcmComfortCruiser};AcmeBicycleShop.options = { ‘headlight‘:HeadlightDecorator, ‘taillight‘:‘TaillightDecorator‘, ‘Bell‘:BellDecorator}