读书笔记之 - javascript 设计模式 - 装饰者模式

本章讨论的是一种为对象增添特性的技术,它并不使用创建新子类这种手段。

装饰者模式可以透明地把对象包装在具有同样接口的另一对象之中,这样一来,你可以给一些方法添加一些行为,然后将方法调用传递给原始对象。相对于创建子类来说,使用装饰者模式对象是一种更灵活的选择。

装饰者可用于为对象增加功能。它可以用来替代大量子类。

考虑前面的自行车类,你现在可能提供一些配件供用户选择,装饰者模式要求我们只需要创建选件类,这些类与四种自行车类都要实现Bicycle接口,但是他们只被用作这些自行车类的包装类。在这个例子中,选件类就是装饰者,而自行车类是它的组件。装饰者对其组件进行了透明包装,二者可以互换使用。这是因为他们使用了同样的接口。下面我们来看应该怎样实现自行车装饰者类。

var Bicycle = new Interface('Bicycle',['assemble','wash','ride','repair','getPrice'])

所有自行车类和选件装饰者都要实现这个接口,AcmeComfortCruiser 类大致就是这个样子

// The AcmeComfortCruiser class

var 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.00 myBicycle = new TaillightDecorator(myBicycle);
alert(myBicycle.getPrice()); //408.00 myBicycle = new HeadlightDecorator(myBicycle);
alert(myBicycle.getPrice()); //423.00

你可以如法炮制,创建无数个装饰者。通过在运行期间动态应用装饰者,你可以创建出具有所有需要特性的对象,而不用去维护那100个不同的子类。要是前灯的价格发生变化,你只需要在HeadlightDecorator类这个地方更新即可,维护工作则容易的多。

装饰者模式颇多得益于接口的使用,因此不必对代码进行修改,这是通过确保所有装饰者对象都实现了Bicycle接口而达到的。接口在此发挥着俩个方面的作用,首先它说明了装饰者必须实现哪些方法,其次它还可以在新版工厂方法中用来确保所创建对象都实现了必须的方法。

如果装饰者对象与其组件不能互换使用,它就丧失了其功用。这是装饰者模式的关键特点。

装饰者模式和组合模式有许多共同点和区别:

  1. 装饰者对象和组合对象都是用来包装别的对象。组合模式中称其为子对象,装饰者模式中称其为组件对象。他们都与所包装的对象实现同样的接口并且会把任何方法调用传递给这些对象。
  2. 组合模式是一种结构型的模式,用于把众多子对象组织为一个整体。藉此程序员与大批对象打交道时可以将他们当做一个对象来对待,并将它们组织为层次性的树。通常它并不修改方法调用,而只是将沿组合对象与子对象的链向下传递,直到到达并落实在叶对象上。
  3. 装饰者模式也是一种结构模式,它并非用于组织对象,而是用于在不修改现有对象或从其派生子类的前提下为其增填职责。在一些简单例子中,装饰者会透明而不加修改地传递所有调用方法,不过,创建装饰者的目的就是在于对方法进行修改。
  4. 尽管简单的组合对象可等同简单的装饰者,这二者却有着不同的焦点,组合对象并不修改方法调用,其着眼点在于组织子对象。而装饰者存在唯一目的就是修改方法调用而不是组织子对象,因为子对象只有一个,虽然这俩种结构相似,但是任务不同。

装饰者修改其组件的方式:

1.在方法之后添加行为。

在方法之后添加行为是最常见的修改方法的做法。具体而言就是先调用组件的方法,并在其返回后实施一些附加行为。例子:

TaillightDecorator.prototype.getPrice = function(){
return this.bicyle.getPrice()+9.00;
}

这个例子中,首先对组件调用getPrice,然后再把前灯的价钱加在该方法调用所返回的价钱上,最后将其结果作为总价返回。这一过程可以根据需要多次重复,作为示范,下面将创建一辆带着俩个前灯和一个尾灯的自行车。

var myBicycle = new AcmeComfortCruiser();
alert(myBicycle.getPrice()); //399.00 myBicycle = 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
}

如果顺序很重要,可以添加一些代码,在使用选件数组之前对其排序。用工厂实例化自行车对象有许多好处,首先,不必了解自行车和装饰者的各种类名,所有这些都封装在AcmeBicycleShop类中,因此填加自行车型号和选件非常容易,只有把他们添加到AcmeBicycleShop.models或AcmeBicycleShop.options数组中即可,下面有使用工厂和不使用工厂的俩种简单比较:

//不使用工厂方法
var myBicycle = new AcmeBicycleShop();
myBicycle = new FrameColorDecorator(myBicycle, 'blue');
myBicycle = new HeadlightDecorator(myBicycle); //使用工厂方法
var alecsCruisers = new AcmeBicycleShop();
var myBicycle = alecsCruisers.createBicycle('The Speedster',[
{name:'color',arg:'bule'},
{name:'headlight'}
]);

该工厂对象对经历了最后一道装饰的对象接口检查,以确保它实现了正确的接口。如果有必要的话,工厂可以对选件进行排序。某些装饰者修改组件方法的方式决定论他们需要最先或最后运用,在此情况下工厂这种作用尤其有用。那种会替换组件方法而不是对其扩充的装饰者需要放在最后创建,以确保其成为最外层的装饰者。

函数装饰者

装饰者并不局限于类。你也可以常见到用来包装队列的函数和方法的装饰者。

下面是一个简单的函数装饰者的例子,这个装饰者包装了另一个函数,其作用在于将被包装者的返回结果改成大写形式。

function upperCaseDecorator(func){
return function(){
return func.apply(this,arguments).toUpperCase();
}
}

这个装饰者可以用来创建新函数,后者执行起来与普通函数没有任何不同。下面例子先定义一个普通函数,然后将其装饰为一个新函数:

function getDate(){
return (new Date()).toString();
}

前面那个函数装饰者的定义,func.apply的作用在于执行被包装的函数,这意味着它也可以用来包装方法:

BellDecorator.prototype.ringBellLoudly = upperCaseDecorator(BellDecorator.prototype.ringBell);
var myBicycle = new AcmeComfprtCruiser();
myBicycle = new BellDecorator(myBicycle);

函数装饰者在对另一个函数的输出应用某种格式或者执行某种转化很有用处。函数装饰者给程序员带了了极大的灵活性,而实现它所需要的代码比大而全的装饰者类少的多。

如果需要为类增添特性或者职责,而从该类派生子类的解决办法并不实际的话,就应该使用装饰者模式。派生子类之所以不实际,最常见的原因是需要增添的特性的数量和组合要求使用大量子类。如果需要为对象增添特性而又不想改变使用该对象的代码的话,也可以采用装饰者模式,因为装饰者可以动态而且透明的修改对象。所以它很适合修改现有系统完成这一任务。

方法性能分析器

装饰者模式擅长于为各种对象增添新特性。本例要创建的装饰者可以用来包装任何对象,以便为其提供方法性能分析功能。这个装饰者必须完全透明,这样他才能应用于任何对象而不干扰其正常的代码,我们将先创建一个实现了计时功能的速成版装饰者,然后将其改为通用型装饰者。

//ListBuilder 是一个测试样例,其功能就是在网页上创建一个有序列表。

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);
}
}
}

我们首先要创建一个专门用于这个ListBuilder类的装饰者,他将记录bulidList方法所耗用的时间。

var SimpleProfiler = function (component) {
this.component = component;
};
SimpleProfiler.prototype = {
buildList: function () {
var startTime = new Date();
this.component.buildList();
var elspsedTime = (new Date().getTime()-startTime.getTime());
console.log(elspsedTime);
}
}

SimpleProfiler 是 ListBuilder的一个装饰者。它也实现了ListBuilder中的buildList这个方法,并且在传递该方法调用的语句前后添加了一些计时代码。测试代码如下:

var list = new ListBuilder('list-container',5000);
list = new SimpleProfiler(list);
list.buildList();

了解装饰者的工作机制之后,现在要考虑的是如何对其进行通用化改造,使其可用于任何对象。为了达到这个目的,必须让装饰者逐一检查组件对象的所有属性,为找到的每一个方法创建一个通道方法。这些通道方法也必须包含启动和停止计时器代码。

var MethodProfiler = function (component) {
this.conponent = component;
this.timers = {};
for (var key in this.conponent) {
if (typeof this.conponent[key]!=="function") {
continue;
}
//Add the method.
var _this = this;
(function(methodName) {
_this[methodName] = function () {
_this.startTimer(methodName);
var returnValue = _this.conponent[methodName].apply(_this.component,arguments);
_this.displayTime(methodName, _this.getElapsedTime(methodName));
return returnValue;
};
})(key);
}
};
MethodProfiler.prototype = {
startTimer: function (methodName) {
this.timers[methodName] = (new Date()).getTime();
},
getElapsedTime:function(methodName){
return (new Date()).getTime() - this.timers[methodName]
},
displayTime:function(methodName,time){
console.log(time+'ms');
}
}

仔细查看那个构造函数,其中的for in 循环。这个循环逐一检查组件对象的每一个属性,跳过不是方法的属性,如果遇到方法属性,则为装饰者添加一个同名的新方法。这样添加的新方法中的代码会启动计时器,调用组件的同名方法、停止并显示计时器以及返回先前保存下来的组件的同名方法的返回值。这种方法的声明被包装在一个匿名函数中,以保留正确的methodName变量值。这个ListBuilder与其第一个版本已经有了足够大的差别,用一个MethodProfiler来装饰它。

var list = new ListBuilder('list-container',5000);
list = new MethodProfiler(list);
list.buildList('ol');
list.buildList('ul');

这个例子出色的应用了装饰者模式,这个性能分析器完全透明,它可以为各种对象添加功能,为此并不需要从哪些对象派生子类。

装饰者模式是在运行期间为对象添加特性或职责的有力工具,而且装饰者的运作过程是透明的,这就是说你可以用它包装其他对象,然后继续按之前使用哪些对象的方法来使用它。但是装饰者模式的缺点主要是表现在俩个方面,首先,在遇到装饰者包装起来的对象时,哪些依赖类型检查的代码会出问题,尽管js中很少使用严格类型的检查,但是如果你的代码中执行了这样的检查,那么装饰者是无法匹配所需要的类型的。第二就是装饰者模式会增加代码结构的复杂度。

上一篇:day-5 python协程与I/O编程深入浅出


下一篇:判断字符串是否是数字类型