裝飾器是旨在提升重用性能的一種結(jié)構(gòu)性設計模式。同Mixin類似,它可以被看作是應用子類劃分的另外一種有價值的可選方案。
典型的裝飾器提供了向一個系統(tǒng)中現(xiàn)有的類動態(tài)添加行為的能力。其創(chuàng)意是裝飾本身并不關心類的基礎功能,而只是將它自身拷貝到超類之中。
它們能夠被用來在不需要深度改變使用它們的對象的依賴代碼的前提下,變更我們希望向其中附加功能的現(xiàn)有系統(tǒng)之中。開發(fā)者使用它們的一個通常的理由是,它們的應用程序也許包含了需要大量彼此不相干類型對象的特性。想象一下不得不要去定義上百個不同對象的構(gòu)造器,比方說,一個Javascript游戲。
對象構(gòu)造器可以代表不同播放器類型,每一種類型具有不同的功能。一種叫做領主戒指的游戲會需要霍比特人、巫術(shù)師,獸人,巨獸,精靈,山嶺巨人,亂世陸地等對象的構(gòu)造器,而這些的數(shù)量很容易過百。而我們還要考慮為每一個類型的能力組合創(chuàng)建子類。
例如,帶指環(huán)的霍比特人,帶劍的霍比特人和插滿寶劍的陸地等等。這并不是非常的實用,當我們考慮到不同能力的數(shù)量在不斷增長這一因素時,最后肯定是不可控的。
裝飾器模式并不去深入依賴于對象是如何創(chuàng)建的,而是專注于擴展它們的功能這一問題上。不同于只依賴于原型繼承,我們在一個簡單的基礎對象上面逐步添加能夠提供附加功能的裝飾對象。它的想法是,不同于子類劃分,我們向一個基礎對象添加(裝飾)屬性或者方法,因此它會是更加輕巧的。
向Javascript中的對象添加新的屬性是一個非常直接了當?shù)倪^程,因此將這一特定牢記于心,一個非常簡單的裝飾器可以實現(xiàn)如下:
// A vehicle constructor
function vehicle( vehicleType ){
// some sane defaults
this.vehicleType = vehicleType || "car";
this.model = "default";
this.license = "00000-000";
}
// Test instance for a basic vehicle
var testInstance = new vehicle( "car" );
console.log( testInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
// Lets create a new instance of vehicle, to be decorated
var truck = new vehicle( "truck" );
// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
this.model = modelName;
};
truck.setColor = function( color ){
this.color = color;
};
// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );
console.log( truck );
// Outputs:
// vehicle:truck, model:CAT, color: blue
// Demonstrate "vehicle" is still unaltered
var secondInstance = new vehicle( "car" );
console.log( secondInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
這種類型的簡單實現(xiàn)是實用的,但它沒有真正展示出裝飾能夠貢獻出來的全部潛能。為這個,我們首先區(qū)分一下我的Coffee示例和Freeman,Sierra和Bates所著Head First Design Patterns這一本優(yōu)秀的書中圍繞Mackbook商店建立的模型,這兩個之間的不同。
// The constructor to decorate
function MacBook() {
this.cost = function () { return 997; };
this.screenSize = function () { return 11.6; };
}
// Decorator 1
function Memory( macbook ) {
var v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}
// Decorator 2
function Engraving( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 200;
};
}
// Decorator 3
function Insurance( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 250;
};
}
var mb = new MacBook();
Memory( mb );
Engraving( mb );
Insurance( mb );
// Outputs: 1522
console.log( mb.cost() );
// Outputs: 11.6
console.log( mb.screenSize() );
在上面的示例中,我們的裝飾器重載了超類對象MacBook()的 object.cost()函數(shù),使其返回的Macbook的當前價格加上了被定制后升級的價格。
這被看做是對原來的Macbook對象構(gòu)造器方法的裝飾,它并沒有將其重寫(例如,screenSize()),我們所定義的Macbook的其它屬性也保持不變,完好無缺。
上面的示例并沒有真正定義什么接口,而且我們也轉(zhuǎn)移了從創(chuàng)造者到接受者移動時確保一個對象對應一個接口的責任。
我們現(xiàn)在要來試試首見于Dustin Diaz與Ross Harmes合著的Pro Javascript Design Patterns(PJDP)中一種裝飾器的變體。
不像早些時候的一些實例,Diaz和Harms堅持更加近似于其他編程語言(如Java或者C++)如何使用一種“接口”的概念來實現(xiàn)裝飾器,我們不久就將對此進行詳細的定義。
注意:裝飾模式的這一特殊變體是提供出來做參考用的。如果發(fā)現(xiàn)它過于復雜,建議你選擇前面更加簡單的實現(xiàn)。
PJDP所描述的裝飾器是一種被用于將具備相同接口的對象進行透明封裝的對象,這樣一種模式。接口是一種定義一個對象應該具有哪些方法的途徑,然而,它實際上并不指定那些方法應該如何實現(xiàn)。
它們也可以聲明方法應該有些什么參數(shù),但這被看做是可選項。
因此,為什么我們要在Javascript中使用接口呢?這個想法意在讓它們具有自說明文檔特性,并促進其重用性。在理論上,接口通過確保了其被改變的同時也要讓其對象實現(xiàn)這些改變,從而使得代碼更加的穩(wěn)定。
下面是一個在Javascript中使用鴨式類型來實現(xiàn)接口的示例,鴨式類型是一種基于所實現(xiàn)的方法來幫助判定一個對象是否是一種構(gòu)造器/對象的實體的方法。
// Create interfaces using a pre-defined Interface
// constructor that accepts an interface name and
// skeleton methods to expose.
// In our reminder example summary() and placeOrder()
// represent functionality the interface should
// support
var reminder = new Interface( "List", ["summary", "placeOrder"] );
var properties = {
name: "Remember to buy the milk",
date: "05/06/2016",
actions:{
summary: function (){
return "Remember to buy the milk, we are almost out!";
},
placeOrder: function (){
return "Ordering milk from your local grocery store";
}
}
};
// Now create a constructor implementing the above properties
// and methods
function Todo( config ){
// State the methods we expect to be supported
// as well as the Interface instance being checked
// against
Interface.ensureImplements( config.actions, reminder );
this.name = config.name;
this.methods = config.actions;
}
// Create a new instance of our Todo constructor
var todoItem = Todo( properties );
// Finally test to make sure these function correctly
console.log( todoItem.methods.summary() );
console.log( todoItem.methods.placeOrder() );
// Outputs:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store
在上面的代碼中,接口確保了實現(xiàn)提供嚴格的功能檢查,而這個和接口構(gòu)造器的接口代碼能在這里找到。
使用接口最大的問題是,由于這并不是Javascript內(nèi)置的對它們的支持,對我們而言就會存在嘗試去模仿另外一種語言的特性,但看著并不完全合適,這樣一種風險。然而對于沒有太大性能消耗的輕量級接口是可以被使用的,并且下面我們將要看到的抽象裝飾器同樣使用了這個概念。
為了闡明這個版本的裝飾者模式的結(jié)構(gòu),我們想象有一個超級類,還是一個Macbook模型,以及一個store,使我們可以用耗費額外費用的許多種增強來“裝飾”Macbook。
增強可以包括升級到4GB或8GB的Ram,雕刻,或相似案例。如果現(xiàn)在我們要針對每一種增強選項的組合,使用單獨的子類進行建模,可能看起來是這樣的:
var Macbook = function(){
//...
};
var MacbookWith4GBRam = function(){},
MacbookWith8GBRam = function(){},
MacbookWith4GBRamAndEngraving = function(){},
MacbookWith8GBRamAndEngraving = function(){},
MacbookWith8GBRamAndParallels = function(){},
MacbookWith4GBRamAndParallels = function(){},
MacbookWith8GBRamAndParallelsAndCase = function(){},
MacbookWith4GBRamAndParallelsAndCase = function(){},
MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};
等等。
這不是一個實際的解決方案,因為一個新的子類可能需要具有每一種可能的增強組合。由于我們傾向于保持事物簡單,不想維持一個巨大的子類集合,我們來看看怎樣用裝飾者更好的解決這個問題。
不需要我們前面看到的所有組合,我們只需要簡單的創(chuàng)建五個新的裝飾者類。對這些增強類的方法調(diào)用,將會傳遞給Macbook類。
在我們下一個例子中,裝飾者透明的包裝了它們的組件,而且有趣的是,可以在相同的接口互換。
這里是我們給Macbook定義的接口:
var Macbook = new Interface( "Macbook",
["addEngraving",
"addParallels",
"add4GBRam",
"add8GBRam",
"addCase"]);
// A Macbook Pro might thus be represented as follows:
var MacbookPro = function(){
// implements Macbook
};
MacbookPro.prototype = {
addEngraving: function(){
},
addParallels: function(){
},
add4GBRam: function(){
},
add8GBRam:function(){
},
addCase: function(){
},
getPrice: function(){
// Base price
return 900.00;
}
};
為了使得我們稍后更加容易的添加所需的更多選項,一種帶有被用來實現(xiàn)Mackbook接口的默認方法的抽象裝飾器方法被定義了出來,其剩余的選項將會進行子類劃分。抽象裝飾器確保了我們能夠獨立于盡可能多的在不同的組合中所需的裝飾器,去裝飾一個基礎類(記得早先的那個示例么?),而不需要去為了每一種可能的組合而去驅(qū)動一個類。
// Macbook decorator abstract decorator class
var MacbookDecorator = function( macbook ){
Interface.ensureImplements( macbook, Macbook );
this.macbook = macbook;
};
MacbookDecorator.prototype = {
addEngraving: function(){
return this.macbook.addEngraving();
},
addParallels: function(){
return this.macbook.addParallels();
},
add4GBRam: function(){
return this.macbook.add4GBRam();
},
add8GBRam:function(){
return this.macbook.add8GBRam();
},
addCase: function(){
return this.macbook.addCase();
},
getPrice: function(){
return this.macbook.getPrice();
}
};
上述示例中所發(fā)生的是Macbook裝飾器在像組件一樣的使用一個對象。它使用了我們早先定義的Macbook接口,對于每一個方法都調(diào)用了組件上相同的方法。我們現(xiàn)在就能夠只使用Macbook裝飾器來創(chuàng)建我們的選項類了——通過簡單調(diào)用超類的構(gòu)造器和根據(jù)需要可以被重載的方法。
var CaseDecorator = function( macbook ){
// call the superclass's constructor next
this.superclass.constructor( macbook );
};
// Let's now extend the superclass
extend( CaseDecorator, MacbookDecorator );
CaseDecorator.prototype.addCase = function(){
return this.macbook.addCase() + "Adding case to macbook";
};
CaseDecorator.prototype.getPrice = function(){
return this.macbook.getPrice() + 45.00;
};
如我們所見,大多數(shù)都是相對應的直接實現(xiàn)。我們所做的是重載需要被裝飾的addCase()和getPrise()方法,而我們通過首先執(zhí)行組件的方法然后將其添加到它里面,來達到目的。
鑒于到目前為止本節(jié)所介紹的信息一斤相當?shù)亩嗔耍屛覀冊囋噷⑵淙糠诺揭粋€單獨的實例中,以期突出我們所學。
// Instantiation of the macbook
var myMacbookPro = new MacbookPro();
// Outputs: 900.00
console.log( myMacbookPro.getPrice() );
// Decorate the macbook
myMacbookPro = new CaseDecorator( myMacbookPro );
// This will return 945.00
console.log( myMacbookPro.getPrice() );
由于裝飾器能夠動態(tài)的修改對象,它們就是改變現(xiàn)有系統(tǒng)的理想模式。有時候,它只是簡單的圍繞一個對象及其維護針對每一個對象類型單獨的子類劃分所產(chǎn)生的麻煩,來創(chuàng)建裝飾器的。這使得維護起可能需要大量子類劃分對象的應用程序來更加顯著的直接。
同我們所涵蓋的其它模式一起,也有許多裝飾器模式的示例能夠使用jQuery來實現(xiàn)。jQuery.extend()允許我們將兩個或者更多個對象(以及它們的屬性)擴展(或者混合)到一個對象中,不論是在運行時或者動態(tài)的在一個稍后的時點上。
在這一場景中,目標對象沒必要打斷或者重載源/超類中現(xiàn)有的方法(盡管這可以被做到)就能夠使用新的功能裝飾起來。 在接下來的示例中,我們定義了三個對象:默認,選項和設置。任務的目標是用在選項中找到的附加功能來裝飾默認對象。
var decoratorApp = decoratorApp || {};
// define the objects we're going to use
decoratorApp = {
defaults: {
validate: false,
limit: 5,
name: "foo",
welcome: function () {
console.log( "welcome!" );
}
},
options: {
validate: true,
name: "bar",
helloWorld: function () {
console.log( "hello world" );
}
},
settings: {},
printObj: function ( obj ) {
var arr = [],
next;
$.each( obj, function ( key, val ) {
next = key + ": ";
next += $.isPlainObject(val) ? printObj( val ) : val;
arr.push( next );
} );
return "{ " + arr.join(", ") + " }";
}
};
// merge defaults and options, without modifying defaults explicitly
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options);
// what we have done here is decorated defaults in a way that provides
// access to the properties and functionality it has to offer (as well as
// that of the decorator "options"). defaults itself is left unchanged
$("#log")
.append( decoratorApp.printObj(decoratorApp.settings) +
+ decoratorApp.printObj(decoratorApp.options) +
+ decoratorApp.printObj(decoratorApp.defaults));
// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); },
// helloWorld: function (){ console.log("hello!"); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log("hello!"); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }
因為它可以被透明的使用,并且也相當?shù)撵`活,因此開發(fā)者都挺樂意去使用這個模式——如我們所見,對象可以用新的行為封裝或者“裝飾”起來,而后繼續(xù)使用,并不用去擔心基礎的對象被改變。在一個更加廣泛的范圍內(nèi),這一模式也避免了我們?nèi)ヒ蕾嚧罅孔宇悂韺崿F(xiàn)同樣的效果。
然而在實現(xiàn)這個模式時,也存在我們應該意識到的缺點。如果窮于管理,它也會由于引入了許多微小但是相似的對象到我們的命名空間中,從而顯著的使得我們的應用程序架構(gòu)變得復雜起來。這里所擔憂的是,除了漸漸變得難于管理,其他不能熟練使用這個模式的開發(fā)者也可能會有一段要掌握它被使用的理由的艱難時期。
足夠的注釋或者對模式的研究,對此應該有助益,而只要我們對在我們的應程序中的多大范圍內(nèi)使用這一模式有所掌控的話,我們就能讓兩方面都得到改善。
更多建議: