1、定義javascript
裝飾者模式可用來透明地把對象包裝在具備一樣接口的另外一個對象之中。這樣一來,你能夠給一個方法添加一些行爲,而後將方法調用傳遞給原始對象。相對於建立子類來講,使用裝飾者對象是一種更靈活的選擇(裝飾者提供比繼承更有彈性的替代方案)。html
裝飾者用於經過重載方法的形式添加新功能,該模式能夠在被裝飾者前面或者後面加上本身的行爲以達到特定的目的。java
2、舉例設計模式
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品牌的電腦,那麼,就須要另外建立三個子類,這樣的設計就過於冗餘複雜。app
2.2 接下來引入工廠模式的例子。上次見到AcmeBicycleShop類的時候,顧客能夠購買的自行車有4種型號。後來這家商店開始爲每一種自行車提供一些額外的特點配件。如今顧客再加點錢就能夠買到帶前燈、尾燈或鈴鐺的自行車。每一種可選配件都會影響到售價和車的組裝方法。咱們使用裝飾者模式來實現該功能。ide
首先,定義一個裝飾者超類——函數
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; };
調用——post
// 一個普通的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".
這個例子出色地應用了裝飾者模式。那個性能分析器徹底透明,它能夠對各類對象添加功能,爲此並不須要從那些對象派生子類。只是用這一個裝飾者類便可垂手可得的對各類各樣的對象進行裝飾。
3、優點
裝飾者是在運行期間爲對象添加特性或指責的有力工具。在自行車商店這個例子中,經過使用裝飾者,你能夠動態地爲自行車對象添加可選的特點配件。在只有部分對象須要這些特性的狀況下裝飾者模式的好處尤其突出。若是不採用這種模式,那麼要想實現一樣的效果必須使用大量子類。裝飾者的運做過程是透明的,這就是說你能夠用它包裝其餘對象,而後繼續按以前使用那些對象的方法來使用它。
4、劣勢
1> 在遇到用裝飾者包裝起來的對象時,那些依賴於類型檢查的代碼會出問題。
2> 使用裝飾者模式每每會增長架構的複雜程度。所以,在設計一個使用了裝飾者模式的架構時,必需要多花點心思,確保本身的代碼有良好的文檔說明,而且容易理解。
5、總結
裝飾者模式是爲已有功能動態地添加更多功能的一種方式,把每一個要裝飾的功能放在單獨的函數裏,而後用該函數包裝所要裝飾的已有函數對象,所以,當須要執行特殊行爲的時候,調用代碼就能夠根據須要有選擇地、按順序地使用裝飾功能來包裝對象。優勢是把類(函數)的核心職責和裝飾功能區分開了。
源自:JavaScript設計模式(人民郵電出版社)——第十二章,裝飾者模式