【读书笔记】读《JavaScript设计模式》之装饰者模式
一、定义
装饰者模式可用来透明地把对象包装在具有同样接口的另一个对象之中。这样一来,你可以给一个方法添加一些行为,然后将方法调用传递给原始对象。相对于创建子类来说,使用装饰者对象是一种更灵活的选择(装饰者提供比继承更有弹性的替代方案)。
装饰者用于通过重载方法的形式添加新功能,该模式可以在被装饰者前面或者后面加上自己的行为以达到特定的目的。
二、举例
2.1 装饰者是一种实现继承的替代方案。当脚本运行时,在子类中添加行为会影响原有类所有的实例,而装饰者却不然。取而代之的是它能给不同对象各自添加新行为。
//需要装饰的类(函数) function Macbook() { this.cost = function () { return 1000; }; } // 装饰——添加内存条 function Memory(macbook) { this.cost = function () { return macbook.cost() + 75; }; } // 装饰——支持蓝光影片驱动 function BlurayDrive(macbook) { this.cost = function () { return macbook.cost() + 300; }; } // 装饰——添加保修 function Insurance(macbook) { this.cost = function () { return macbook.cost() + 250; }; } // 用法 var myMacbook = new Insurance(new BlurayDrive(new Memory(new Macbook()))); console.log(myMacbook.cost()); // 1000 + 75 + 300 + 250
当然,我们也可以通过添加子类的方式,设计三个子类(MemoryMac、BlurayDriveMac、InsuranceMac),复写cost方法。但是,如果将来有另外一个品牌的电脑,如dell品牌的电脑,那么,就需要另外创建三个子类,这样的设计就过于冗余复杂。
2.2 接下来引入工厂模式的例子。上次见到AcmeBicycleShop类的时候,顾客可以购买的自行车有4种型号。后来这家商店开始为每一种自行车提供一些额外的特色配件。现在顾客再加点钱就可以买到带前灯、尾灯或铃铛的自行车。每一种可选配件都会影响到售价和车的组装方法。我们使用装饰者模式来实现该功能。
首先,定义一个装饰者超类——
var BicycleDecorator = function(bicycle) { this.bicycle = 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(); } };
现在来添加一个装饰者类,给自行车添加头灯——
var HeadlightDecorator = function(bicycle) { this.bicycle = bicycle; }; HeadlightDecorator.prototype = new BicycleDecorator(); HeadlightDecorator.prototype.assemble = function() { return this.bicycle.assemble() + ' Attach headlight to handlebars'; }; HeadlightDecorator.prototype.getPrice = function() { return this.bicycle.getPrice() + 15.00; };
调用——
// 一个普通的AcmeComfortCruiser车子 var myBicycle = new AcmeComfortCruiser(); console.log(myBicycle.getPrice()); // 399.00 // 我们为AcmeComfortCruiser的车子添加前置灯之后的自行车售价 myBicycle = new HeadlightDecorator(acmeComfortCruiser); console.log(myBicycle.getPrice()); // 399.00 + 15.00 = 414.00
会发现这里的myBicycle变量被重置为对应的装饰者对象,也就意味着将不能再访问原来的那个自行车对象。不过,没有关系,因为这个装饰者完全可以和自行车对象互换使用。装饰者最重要的特点之一就是它可以用来替代其组件(这里,我们用new HeadlightDecorator(acmeComfortCruiser)替换了new AcmeComfortCruiser()对象)。这是通过确保装饰者和对应组件都实现了Bicycle接口而达到的。如果装饰者对象与其组件不能互换使用,它就是丧失了其功用。要注意防止装饰者和组件出现接口方面的差异。这种模式的好处之一就是可以透明地用新对象装饰现有的独享,而这并不会改变代码中的其他东西。只有装饰者和组件实现了同样的接口才能做到这一点。
添加尾灯的装饰者——
var TaillightDecorator = function(bicycle) { // implements Bicycle this.superclass.constructor(bicycle); // Call the superclass's constructor. } extend(TaillightDecorator, BicycleDecorator); // Extend the superclass. TaillightDecorator.prototype.assemble = function() { return this.bicycle.assemble() + ' Attach taillight to the seat post.'; }; TaillightDecorator.prototype.getPrice = function() { return this.bicycle.getPrice() + 9.00; };
应用——添加两个头灯,一个尾灯——
var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. alert(myBicycle.getPrice()); // Returns 399.00 myBicycle = new TaillightDecorator(myBicycle); // Decorate the bicycle object // with a taillight. alert(myBicycle.getPrice()); // Now returns 408.00
会发现,可以为我的自行车实例,不断添加各种装饰。这样呢,也就实现了为自行车对象添加各种配件的需求。
2.3 装饰者修改其组件的方式,有——
1> 在方法之前添加
var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. alert(myBicycle.getPrice()); // Returns 399.00 myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object // with the first headlight. myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object // with the second headlight. myBicycle = new TaillightDecorator(myBicycle); // Decorate the bicycle object // with a taillight. alert(myBicycle.getPrice()); // Now returns 438.00
2> 在方法之后添加 - 添加车架颜色的装饰
var FrameColorDecorator = function(bicycle, frameColor) { // implements Bicycle this.superclass.constructor(bicycle); // Call the superclass's constructor. this.frameColor = frameColor; } // extend(FrameColorDecorator, BicycleDecorator); // Extend the superclass. 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(); // Instantiate the bicycle. myBicycle = new FrameColorDecorator(myBicycle, 'red'); // Decorate the bicycle // object with the frame color. myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object // with the first headlight. myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object // with the second headlight. myBicycle = new TaillightDecorator(myBicycle); // Decorate the bicycle object // with a taillight. alert(myBicycle.assemble()); /* Returns: "Paint the frame red and allow it to dry. (Full instructions for assembling the bike itself go here) Attach headlight to handlebars. Attach headlight to handlebars. Attach taillight to the seat post." */
3> 替换方法
有时为了实现新行为必须对方法进行整体替换。在此情况下,组件方法不会被调用(或者虽然被调用但其返回值会被抛弃)。作为这种修改的一个例子,下面我们将创建一个用来实现自行车的终生保修的装饰者。
var LifetimeWarrantyDecorator = function(bicycle) { // implements Bicycle this.superclass.constructor(bicycle); // Call the superclass's constructor. } // extend(LifetimeWarrantyDecorator, BicycleDecorator); // Extend the superclass. // 这里的维修方法不再调用组件的repair方法 LifetimeWarrantyDecorator.prototype.repair = function() { return 'This bicycle is covered by a lifetime warranty. Please take it to ' + 'an authorized Acme Repair Center.'; }; LifetimeWarrantyDecorator.prototype.getPrice = function() { return this.bicycle.getPrice() + 199.00; };
4> 添加新方法
var BellDecorator = function(bicycle) { // implements Bicycle this.superclass.constructor(bicycle); // Call the superclass's constrcutor. } extend(BellDecorator, BicycleDecorator); // Extend the superclass. BellDecorator.prototype.assemble = function() { return this.bicycle.assemble() + ' Attach bell to handlebars.'; }; BellDecorator.prototype.getPrice = function() { return this.bicycle.getPrice() + 6.00; }; BellDecorator.prototype.ringBell = function() { return 'Bell rung.'; }; // 添加按铃 var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. myBicycle = new BellDecorator(myBicycle); // Decorate the bicycle object // with a bell. alert(myBicycle.ringBell()); // Returns 'Bell rung.' // 但是BellDecorator必须放在最后应用,否则这个新方法将无法访问 var myBicycle = new AcmeComfortCruiser(); // Instantiate the bicycle. myBicycle = new BellDecorator(myBicycle); // Decorate the bicycle object // with a bell. myBicycle = new HeadlightDecorator(myBicycle); // Decorate the bicycle object // with a headlight. alert(myBicycle.ringBell()); // Method not found.
2.4 函数装饰者
装饰者并不局限于类。你也可以创建用来包装独立的函数和方法的装饰者。
// 将包装者的返回结果改为大写形式 function upperCaseDecorator(func) { return function() { return func.apply(this, arguments).toUpperCase(); } } function getDate() { return (new Date()).toString(); } getDateCaps = upperCaseDecorator(getDate); alert(getDate()); // Returns Wed Sep 26 2007 20:11:02 GMT-0700 (PDT) alert(getDateCaps()); // Returns WED SEP 26 2007 20:11:02 GMT-0700 (PDT)
函数装饰者在对另一个函数的输出应用某种格式或执行某种转换这方面很有用处。
下面演示,为对应的组件添加一个代码执行时间的装饰——
// 添加计时器 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'); } }; /* Usage. */ var list = new ListBuilder('list-container', 5000); // Instantiate the object. list = new SimpleProfiler(list); // Wrap the object in the decorator. list.buildList(); // Creates the list and displays "buildList: 298 ms".
我们对这个代码执行装饰器,进行进一步抽象——
var MethodProfiler = function(component) { var that = this; this.component = component; this.timers = {}; for(var key in this.component) { // Ensure that the property is a function. if(typeof this.component[key] !== 'function') { continue; } // Add the method. (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[methodName] = (new Date()).getTime(); }, getElapsedTime: function(methodName) { return (new Date()).getTime() - this.timers[methodName]; }, displayTime: function(methodName, time) { console.log(methodName + ': ' + time + ' ms'); } }; /* Usage. */ var list = new ListBuilder('list-container', 5000); list = new MethodProfiler(list); list.buildList('ol'); // Displays "buildList: 301 ms". list.buildList('ul'); // Displays "buildList: 287 ms". list.removeLists('ul'); // Displays "removeLists: 10 ms". list.removeLists('ol'); // Displays "removeLists: 12 ms".
这个例子出色地应用了装饰者模式。那个性能分析器完全透明,它可以对各种对象添加功能,为此并不需要从那些对象派生子类。只是用这一个装饰者类即可轻而易举的对各种各样的对象进行装饰。
三、优势
装饰者是在运行期间为对象添加特性或指责的有力工具。在自行车商店这个例子中,通过使用装饰者,你可以动态地为自行车对象添加可选的特色配件。在只有部分对象需要这些特性的情况下装饰者模式的好处尤为突出。如果不采用这种模式,那么要想实现同样的效果必须使用大量子类。装饰者的运作过程是透明的,这就是说你可以用它包装其他对象,然后继续按之前使用那些对象的方法来使用它。
四、劣势
1> 在遇到用装饰者包装起来的对象时,那些依赖于类型检查的代码会出问题。
2> 使用装饰者模式往往会增加架构的复杂程度。因此,在设计一个使用了装饰者模式的架构时,必须要多花点心思,确保自己的代码有良好的文档说明,并且容易理解。
五、总结
装饰者模式是为已有功能动态地添加更多功能的一种方式,把每个要装饰的功能放在单独的函数里,然后用该函数包装所要装饰的已有函数对象,因此,当需要执行特殊行为的时候,调用代码就可以根据需要有选择地、按顺序地使用装饰功能来包装对象。优点是把类(函数)的核心职责和装饰功能区分开了。
源自:JavaScript设计模式(人民邮电出版社)——第十二章,装饰者模式
参考:深入理解JavaScript系列(29):设计模式之装饰者模式