裝飾器模式
裝飾器模式是一種旨在提升代碼復(fù)用率的結(jié)構(gòu)性模式。有點(diǎn)類似于混入模式,它被認(rèn)為是一種可以替代子類的可行方案。
一般來(lái)說(shuō),裝飾器提供一種動(dòng)態(tài)的為系統(tǒng)中的類添加行為的能力。裝飾器本身沒(méi)必要有類的基本功能,否則它自己就可以作為父類了。
他們通??梢员挥脕?lái)修改現(xiàn)存的系統(tǒng),為一些對(duì)象加入一些額外的功能(特性),而不用大量修改底層代碼。開(kāi)發(fā)人員使用他們的一個(gè)常見(jiàn)原因可能包含特征——需要大量不同類型的對(duì)象。想象一下要定義幾百種不同的對(duì)象構(gòu)造函數(shù),比如一個(gè)js游戲。
對(duì)象構(gòu)造函數(shù)可能表示不同類型的角色類型,每個(gè)角色都有不同的能力。魔戒這款游戲可能需要霍比特人,精靈,獸人,精靈,山嶺巨人,石巨人等等,這些很容易有成百上千個(gè)。如果我們?cè)倏紤]能力,想象得為每種能力的結(jié)合創(chuàng)建一個(gè)子類,例如帶指環(huán)的霍比特人,帶劍的霍比特人,帶指環(huán)和劍的霍比特人等等。當(dāng)我們考慮伴隨著能力類型數(shù)量的增長(zhǎng),這是非常不實(shí)用且不可想象的。
裝飾器模式?jīng)]有過(guò)分的關(guān)注如何對(duì)象如何被創(chuàng)建,而是如何擴(kuò)展他們的功能。相比較原型繼承,我們用一個(gè)單一的基本對(duì)象,并逐步第為它增加提供額外能力的對(duì)象的方式工作。這個(gè)想法相比較子類,通過(guò)增加一些屬性或者方法到基本對(duì)象上,可以更加精簡(jiǎn)。
為js對(duì)像添加新屬性是一個(gè)非常直接的過(guò)程,考慮著一點(diǎn),一個(gè)非常簡(jiǎn)單的裝飾器可能實(shí)現(xiàn)如下:
案例1:具有新功能的裝飾構(gòu)造器
// 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
這種最簡(jiǎn)單的實(shí)現(xiàn)是有多種用途的,但是并沒(méi)有真正的展示裝飾器提供的全部力量。對(duì)于這一點(diǎn),我們將首先完成一個(gè)優(yōu)秀的案例。
案例2:用多個(gè)裝飾器裝飾一個(gè)對(duì)象
// 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() );
In the above example, our Decorators are overriding the MacBook() super-class objects .cost() function to return the current price of the Macbook plus the cost of the upgrade being specified.
在上面的例子中,我們就要重寫(xiě)了macbook()父類對(duì)象的cost()函數(shù)以返回的MacBook升級(jí)特定部分后加總的目前價(jià)格。
裝飾器只修改原對(duì)象的部分。
偽-經(jīng)典 裝飾器
這種裝飾器模式的特別變種提供參考用途,如果發(fā)現(xiàn)它太過(guò)復(fù)雜,我建議選擇一個(gè)之前提到過(guò)的簡(jiǎn)單實(shí)現(xiàn)。
接口
PJDP 描述裝飾器作為一種模式被用來(lái),將對(duì)象透明的包裝在其他具有相同接口中的對(duì)象中。接口是一種一個(gè)對(duì)象的方法應(yīng)該做什么的定義方式。然而,他并不指明如何實(shí)現(xiàn)這些方法。
他們可以指明方法所使用的參數(shù),但這被認(rèn)為是可選的。
那么,我們?yōu)槭裁匆趈s中使用接口呢?我們的像是,接口自身就是種記錄,并且可以提高復(fù)用性。理論上說(shuō),接口也使得代碼更穩(wěn)定,通過(guò)確保改變他們也必須改變實(shí)現(xiàn)他們的對(duì)象。
以下是一種通過(guò)js鴨式編程(一種方法,幫助決定一個(gè)對(duì)象是否是一個(gè)構(gòu)造函數(shù)/基于構(gòu)造函數(shù)生成對(duì)象的實(shí)例,的實(shí)現(xiàn))方式實(shí)現(xiàn)的接口的例子。
// 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 = new 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
上面的代碼中,Interface.ensureImplements確保嚴(yán)格的功能檢查,關(guān)于Interface實(shí)現(xiàn)可以看這里:[URL:https://gist.github.com/1057989]
使用接口最大的問(wèn)題是,js并沒(méi)提供內(nèi)置的接口支持,當(dāng)我們?cè)噲D模擬其他語(yǔ)言的特性的有可能是不合適的。清理的接口可能不會(huì)造成太大的性能開(kāi)銷,我們接下來(lái)將看一下使用相同概念的抽象裝飾器模式。
抽象裝飾器
為了展示這個(gè)版本的的裝飾模式,我們準(zhǔn)備再次想象有個(gè)父類模型Macbook,和一個(gè)允許我們付費(fèi)升級(jí)我們Macbook的商店。
升級(jí)可以包括,4GB Ram到8GB Ram,雕刻或者其他。如果我們的模型使用獨(dú)立的子類組合各種可能的升級(jí)選項(xiàng),那么將看起來(lái)像這樣:
var Macbook = function(){
//...
};
var MacbookWith4GBRam = function(){},
MacbookWith8GBRam = function(){},
MacbookWith4GBRamAndEngraving = function(){},
MacbookWith8GBRamAndEngraving = function(){},
MacbookWith8GBRamAndParallels = function(){},
MacbookWith4GBRamAndParallels = function(){},
MacbookWith8GBRamAndParallelsAndCase = function(){},
MacbookWith4GBRamAndParallelsAndCase = function(){},
MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};
這是一個(gè)不切實(shí)際的解決方案,作為一個(gè)新的子類將需要每一個(gè)可能的組合。因?yàn)槲覀兿矚g將事情簡(jiǎn)單化而不是維護(hù)一堆的子類,讓我們看看裝飾器模式如何更好的幫我們解決這個(gè)問(wèn)題。
相比要求我們?cè)缙诳吹降娜拷M合,我們只需要?jiǎng)?chuàng)建五個(gè)裝飾器類。在這些加強(qiáng)類上調(diào)用的方法將被傳遞給我們的Macbook類。
在下一個(gè)例子中,裝飾器透明的包裝他們的組件,并且有趣的是可以使用相同的接口互換他們。
這是我們將定義的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àng),一個(gè)抽象抽象的裝飾器被定義,并且需要默認(rèn)實(shí)現(xiàn)Macbook類所定義的接口,剩余的選項(xiàng)講需要子類。抽象裝飾器確保我們可以裝飾一個(gè)基本類獨(dú)立于為了不同的組合而存在許多裝飾器,不需要為每一可能的組合派生出一個(gè)類。
// 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();
}
};
上面的示例中Macbook裝飾器接受一個(gè)對(duì)象作為我們的基本組件,它使用我們之前定義的Macbook接口并且每個(gè)方法只需要調(diào)用組件上相同的方法。我們現(xiàn)在可以創(chuàng)建我們的選項(xiàng)類,用來(lái)裝飾Macbook。
// First, define a way to extend an object a
// with the properties in object b. We'll use
// this shortly!
function extend( a, b ){
for( var key in b )
if( b.hasOwnProperty(key) )
a[key] = b[key];
return a;
}
var CaseDecorator = function( macbook ){
this.macbook = macbook;
};
// Let's now extend (decorate) the CaseDecorator
// with a MacbookDecorator
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;
};
我們?cè)谶@里重寫(xiě)了需要被裝飾的addCase()和getPrice()方法,并且我們實(shí)現(xiàn)它是通過(guò)首先在原始的Macbook對(duì)象中調(diào)用它。并簡(jiǎn)單的加一個(gè)字符串或者一個(gè)數(shù)字值。
到目前為止有大量的信息被展現(xiàn)在本章節(jié)。讓我們?cè)囍靡粋€(gè)簡(jiǎn)單的案例總結(jié)在一起,這樣有望于突出我們所學(xué)。
// Instantiation of the macbook
var myMacbookPro = new MacbookPro();
// Outputs: 900.00
console.log( myMacbookPro.getPrice() );
// Decorate the macbook
var decoratedMacbookPro = new CaseDecorator( myMacbookPro );
// This will return 945.00
console.log( decoratedMacbookPro.getPrice() );
作為裝飾器可以動(dòng)態(tài)的修改對(duì)象,這對(duì)于修改現(xiàn)存系統(tǒng)是一個(gè)非常棒的模式。有時(shí),他只是簡(jiǎn)單的額創(chuàng)建一個(gè)相關(guān)的對(duì)象,以對(duì)抗為每個(gè)對(duì)象類型維護(hù)一堆子類。這使得需要維護(hù)一大堆子類對(duì)象的應(yīng)用變得更直接。
本例的功能版本可以在JSBin上找到。
jQuery中的裝飾器
正如我們上面所提到的其他模式一樣,這里也有一個(gè)裝飾模式的例子可以通過(guò)jQuery實(shí)現(xiàn)。jQuery.extend()允許我們?cè)诔绦蜻\(yùn)行中擴(kuò)展(或者合并)兩個(gè)或者更多個(gè)對(duì)象到一個(gè)對(duì)象中。
在這種情況下,一個(gè)目標(biāo)對(duì)象可以從原對(duì)象/父類,被裝飾新的功能而不用打破或者重寫(xiě)現(xiàn)有的方法。
在下面的案例中,我們定義了三個(gè)對(duì)象:defaults,options 和settings。我們的目標(biāo)是用在options,settings中的額外功能去裝飾默認(rèn)的對(duì)象。我們必須:
a)保留“default”的狀態(tài)在一種未接觸的狀態(tài)下,即我們不會(huì)在之后的時(shí)間點(diǎn)失去訪問(wèn)它屬性和方法的權(quán)限。
b)獲得options中用來(lái)裝飾屬性和功能的能力。
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 world" ); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log( "hello world" ); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }
優(yōu)點(diǎn)和缺點(diǎn)
開(kāi)發(fā)人員喜歡使用這種模式,因?yàn)樗梢员煌该鞯氖褂谩2⑶乙彩肿杂?,正如我們看到的那樣,?duì)象可以被包裹或者裝飾而含有新的行為,繼續(xù)被使用并且不用擔(dān)心原來(lái)對(duì)象被修改。在更管飯的上下文中,這種模式也可以避免我們需要依賴一大子類而獲得同樣的效用。
當(dāng)然實(shí)現(xiàn)這種模式的時(shí)候我們也應(yīng)該意識(shí)到一些缺點(diǎn)。如果管理不善,它可以顯著的復(fù)雜話我們的應(yīng)用架構(gòu),因?yàn)樗o我們的命名空間進(jìn)入了許多小但是相似的對(duì)象。除了變得難以管理,其他不熟悉這種模式的開(kāi)發(fā)者可能很難了解為什么這種模式被使用。
充足的交流和模式的研究可以對(duì)第二個(gè)問(wèn)題有幫助,然而,我們應(yīng)該控制我們的應(yīng)用程序中裝飾器的廣泛成都,并且統(tǒng)計(jì)他們的全部數(shù)量。