裝飾者模式

玩魔獸爭霸的任務關時,對 15 級亂加技能點的野生英雄廣泛沒有好感,而是喜歡留着
技能點,在遊戲的進行過程當中按需加技能。一樣,在程序開發中,許多時候都並不但願某個類天
生就很是龐大,一次性包含許多職責。那麼咱們就可使用裝飾者模式。裝飾者模式能夠動態地
給某個對象添加一些額外的職責,而不會影響從這個類中派生的其餘對象。
在傳統的面嚮對象語言中,給對象添加功能經常使用繼承的方式,可是繼承的方式並不靈活,
還會帶來許多問題:一方面會致使超類和子類之間存在強耦合性,當超類改變時,子類也會隨之
改變;另外一方面,繼承這種功能複用方式一般被稱爲「白箱複用」,「白箱」是相對可見性而言的,
在繼承方式中,超類的內部細節是對子類可見的,繼承經常被認爲破壞了封裝性。javascript

使用繼承還會帶來另一個問題,在完成一些功能複用的同時,有可能建立出大量的子類,
使子類的數量呈爆炸性增加。好比如今有 4種型號的自行車,咱們爲每種自行車都定義了一個單
獨的類。如今要給每種自行車都裝上前燈、尾
燈和鈴鐺這 3種配件。若是使用繼承的方式來給
每種自行車建立子類,則須要 4×3 = 12 個子類。
可是若是把前燈、尾燈、鈴鐺這些對象動態組
合到自行車上面,則只須要額外增長 3個類。java

這種給對象動態地增長職責的方式稱爲裝
飾者(decorator)模式。裝飾者模式可以在不改
變對象自身的基礎上,在程序運行期間給對象
動態地添加職責。跟繼承相比,裝飾者是一種
更輕便靈活的作法,這是一種「即用即付」的
方式,好比天冷了就多穿一件外套,須要飛行
時就在頭上插一支竹蜻蜓,遇到一堆食屍鬼時
就點開 AOE(範圍攻擊)技能。ajax

1 模擬傳統面嚮對象語言的裝飾者模式
var obj = {
name: 'sven',
address: '深圳市'
};
obj.address = obj.address + '福田區';

傳統面嚮對象語言中的裝飾者模式在 JavaScript中適用的場景並很少,如上面代碼所示,通
常咱們並不太介意改動對象自身。儘管如此,本節咱們仍是稍微模擬一下傳統面嚮對象語言中的
裝飾者模式實現。
假設咱們在編寫一個飛機大戰的遊戲,隨着經驗值的增長,咱們操做的飛機對象能夠升級成
更厲害的飛機,一開始這些飛機只能發射普通的子彈,升到第二級時能夠發射導彈,升到第三級
時能夠發射原子彈。
下面來看代碼實現,首先是原始的飛機類:設計模式

var Plane = function(){}
Plane.prototype.fire = function(){
console.log( '發射普通子彈' );
}

接下來增長兩個裝飾類,分別是導彈和原子彈:app

var MissileDecorator = function( plane ){
this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
this.plane.fire();
console.log( '發射導彈' );
}
var AtomDecorator = function( plane ){
this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
this.plane.fire();
console.log( '發射原子彈' );
}

導彈類和原子彈類的構造函數都接受參數 plane 對象,而且保存好這個參數,在它們的 fire
方法中,除了執行自身的操做以外,還調用 plane 對象的 fire 方法。
這種給對象動態增長職責的方式,並無真正地改動對象自身,而是將對象放入另外一個對象
之中,這些對象以一條鏈的方式進行引用,造成一個聚合對象。這些對象都擁有相同的接口( fire
方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操做,隨後把請求轉發給鏈中的
下一個對象。框架

由於裝飾者對象和它所裝飾的對象擁有一致的接口,因此它們對使用該對象的客戶來講是透
明的,被裝飾的對象也並不須要瞭解它曾經被裝飾過,這種透明性使得咱們能夠遞歸地嵌套任意
多個裝飾者對象。異步

最後看看測試結果:函數

var plane = new Plane();
plane = new MissileDecorator( plane );
plane = new AtomDecorator( plane );
plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
2 裝飾者也是包裝器

在《設計模式》成書以前,GoF原想把裝
飾者(decorator)模式稱爲包裝器(wrapper)
模式。
從功能上而言,decorator能很好地描述這
個模式,但從結構上看,wrapper 的說法更加
貼切。裝飾者模式將一個對象嵌入另外一個對象
之中,實際上至關於這個對象被另外一個對象包
裝起來,造成一條包裝鏈。請求隨着這條鏈依
次傳遞到全部的對象,每一個對象都有處理這條
請求的機會。性能

3 回到 JavaScript 的裝飾者
var plane = {
fire: function(){
console.log( '發射普通子彈' );
}
}
var missileDecorator = function(){
console.log( '發射導彈' );
}
var atomDecorator = function(){
console.log( '發射原子彈' );
}
var fire1 = plane.fire;
plane.fire = function(){
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
4 裝飾函數

在 JavaScript中,幾乎一切都是對象,其中函數又被稱爲一等對象。在平時的開發工做中,
也許大部分時間都在和函數打交道。在 JavaScript中能夠很方便地給某個對象擴展屬性和方法,
但卻很難在不改動某個函數源代碼的狀況下,給該函數添加一些額外的功能。在代碼的運行期間,
咱們很難切入某個函數的執行環境。
要想爲函數添加一些功能,最簡單粗暴的方式就是直接改寫該函數,但這是最差的辦法,直
接違反了開放封閉原則:測試

var a = function(){
alert (1);    
}

// 改爲:
var a = function(){
alert (1);
alert (2);
}

不少時候咱們不想去碰原函數,也許原函數是由其餘同事編寫的,裏面的實現很是雜亂。甚
至在一個古老的項目中,這個函數的源代碼被隱藏在一個咱們不肯碰觸的陰暗角落裏。如今須要
一個辦法,在不改變函數源代碼的狀況下,能給函數增長功能,這正是開放封閉原則給咱們指
出的光明道路。
其實在 3節的代碼中,咱們已經找到了一種答案,經過保存原引用的方式就能夠改寫某個
函數:

var a = function(){
alert (1);
}
var _a = a;
a = function(){
_a();
alert (2);
}
a();

這是實際開發中很常見的一種作法,好比咱們想給 window 綁定 onload 事件,可是又不肯定
這個事件是否是已經被其餘人綁定過,爲了不覆蓋掉以前的 window.onload 函數中的行爲,我
們通常都會先保存好原先的 window.onload ,把它放入新的 window.onload 裏執行:

window.onload = function(){
alert (1);
}
var _onload = window.onload || function(){};
window.onload = function(){
_onload();
alert (2);

}

 必須維護 _onload 這個中間變量,雖然看起來並不起眼,但若是函數的裝飾鏈較長,或者
須要裝飾的函數變多,這些中間變量的數量也會愈來愈多。

 其實還遇到了 this 被劫持的問題,在 window.onload 的例子中沒有這個煩惱,是由於調用
普通函數 _onload 時, this 也指向 window ,跟調用 window.onload 時同樣(函數做爲對象的
方法被調用時, this 指向該對象,因此此處 this 也只指向 window )。如今把 window.onload
換成 document.getElementById ,代碼以下:

var _getElementById = document.getElementById;
document.getElementById = function( id ){
alert (1);
return _getElementById( id ); // (1)
}
var button = document.getElementById( 'button' );

執行這段代碼,咱們看到在彈出 alert(1) 以後,緊接着控制檯拋出了異常:
// 輸出: Uncaught TypeError: Illegal invocation

異常發生在(1) 處的 _getElementById( id ) 這句代碼上,此時 _getElementById 是一個全局函數,
當調用一個全局函數時, this 是指向 window 的,而 document.getElementById 方法的內部實現須要
使用 this 引用, this 在這個方法內預期是指向 document ,而不是 window , 這是錯誤發生的緣由,
因此使用如今的方式給函數增長功能並不保險。
改進後的代碼能夠知足需求,咱們要手動把 document 看成上下文 this 傳入 _getElementById :

var _getElementById = document.getElementById;

document.getElementById = function(){
alert (1);
return _getElementById.apply( document, arguments );
}
var button = document.getElementById( 'button' );

但這樣作顯然很不方便,下面咱們用 AOP,來提供一種完美的方法給
函數動態增長功能。

首先給出 Function.prototype.before 方法和 Function.prototype.after 方法:

Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函數的引用
return function(){ // 返回包含了原函數和新函數的"代理"函數
beforefn.apply( this, arguments ); // 執行新函數,且保證 this 不被劫持,新函數接受的參數
// 也會被原封不動地傳入原函數,新函數在原函數以前執行
return __self.apply( this, arguments ); // 執行原函數並返回原函數的執行結果,
// 而且保證 this 不被劫持
}    
}
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};

Function.prototype.before 接受一個函數看成參數,這個函數即爲新添加的函數,它裝載了
新添加的功能代碼。
接下來把當前的 this 保存起來,這個 this 指向原函數,而後返回一個「代理」函數,這個
「代理」函數只是結構上像代理而已,並不承擔代理的職責(好比控制對象的訪問等)。它的工做
是把請求分別轉發給新添加的函數和原函數,且負責保證它們的執行順序,讓新添加的函數在原
函數以前執行(前置裝飾),這樣就實現了動態裝飾的效果。
咱們注意到,經過 Function.prototype.apply 來動態傳入正確的 this ,保證了函數在被裝飾
以後, this 不會被劫持。
Function.prototype.after 的原理跟 Function.prototype.before 如出一轍,惟一不一樣的地方在
於讓新添加的函數在原函數執行以後再執行

下面來試試用 Function.prototype.before 的威力:

<button id="button"></button>

Function.prototype.before = function( beforefn ){
var __self = this;
return function(){
beforefn.apply( this, arguments );
return __self.apply( this, arguments );
}
}
document.getElementById = document.getElementById.before(function(){
alert (1);
});
var button = document.getElementById( 'button' );


console.log( button );

再回到 window.onload 的例子,看看用 Function.prototype.before 來增長新的 window.onload
事件是多麼簡單:

window.onload = function(){
alert (1);
}
window.onload = ( window.onload || function(){} ).after(function(){
alert (2);
}).after(function(){
alert (3);
}).after(function(){
alert (4);
});

值得提到的是,上面的 AOP實現是在 Function.prototype 上添加 before 和 after 方法,但許
多人不喜歡這種污染原型的方式,那麼咱們能夠作一些變通,把原函數和新函數都做爲參數傳入
before 或者 after 方法:

var before = function( fn, beforefn ){
return function(){
beforefn.apply( this, arguments );
return fn.apply( this, arguments );
}
}
var a = before(
function(){alert (3)},
function(){alert (2)}
);
a = before( a, function(){alert (1);} );
a();
6 AOP 的應用實例
用 AOP裝飾函數的技巧在實際開發中很是有用。不管是業務代碼的編寫,仍是在框架層面,

咱們均可以把行爲依照職責分紅粒度更細的函數,隨後經過裝飾把它們合併到一塊兒,這有助於我
們編寫一個鬆耦合和高複用性的系統。

介紹幾個例子,帶你們進一步理解裝飾函數的威力。

1 數據統計上報

分離業務代碼和數據統計代碼,不管在什麼語言中,都是 AOP的經典應用之一。在項目開發

的結尾階段不免要加上不少統計數據的代碼,這些過程可能讓咱們被迫改動早已封裝好的函數。
好比頁面中有一個登陸 button,點擊這個 button會彈出登陸浮層,與此同時要進行數據上報,
來統計有多少用戶點擊了這個登陸 button:

<button tag="login" id="button">點擊打開登陸浮層</button>

var showLogin = function(){
console.log( '打開登陸浮層' );
log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
console.log( '上報標籤爲: ' + tag );
// (new Image).src = 'http:// xxx.com/report?tag=' + tag; // 真正的上報代碼略
}
document.getElementById( 'button' ).onclick = showLogin;

咱們看到在 showLogin 函數裏,既要負責打開登陸浮層,又要負責數據上報,這是兩個層面
的功能,在此處卻被耦合在一個函數裏。使用 AOP分離以後,代碼以下:

Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
var showLogin = function(){
console.log( '打開登陸浮層' );
}
var log = function(){
console.log( '上報標籤爲: ' + this.getAttribute( 'tag' ) );
}
showLogin = showLogin.after( log ); // 打開登陸浮層以後上報數據
document.getElementById( 'button' ).onclick = showLogin;


咱們看到在 showLogin 函數裏,既要負責打開登陸浮層,又要負責數據上報,這是兩個層面
的功能,在此處卻被耦合在一個函數裏。使用 AOP分離以後,代碼以下:

Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
var showLogin = function(){
console.log( '打開登陸浮層' );
}
var log = function(){
console.log( '上報標籤爲: ' + this.getAttribute( 'tag' ) );
}
showLogin = showLogin.after( log ); // 打開登陸浮層以後上報數據
document.getElementById( 'button' ).onclick = showLogin;

2 用AOP動態改變函數的參數

觀察 Function.prototype.before 方法:

Function.prototype.before = function( beforefn ){
var __self = this;
return function(){
beforefn.apply( this, arguments ); // (1)
return __self.apply( this, arguments ); // (2)
}
}

從這段代碼的(1)處和(2)處能夠看到, beforefn 和原函數 __self 共用一組參數列表
arguments ,當咱們在 beforefn 的函數體內改變 arguments 的時候,原函數 __self 接收的參數列
表天然也會變化。
下面的例子展現瞭如何經過 Function.prototype.before 方法給函數 func 的參數 param 動態地
添加屬性 b :

var func = function( param ){
console.log( param ); // 輸出: {a: "a", b: "b"}
}
func = func.before( function( param ){
param.b = 'b';
});
func( {a: 'a'} );

如今有一個用於發起 ajax請求的函數,這個函數負責項目中全部的 ajax異步請求:

var ajax = function( type, url, param ){
console.dir(param);
// 發送 ajax 請求的代碼略
};
ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } );

上面的僞代碼表示向後臺 cgi 發起一個請求來獲取用戶信息,傳遞給 cgi 的參數是 { name:
'sven' } 。

ajax 函數在項目中一直運轉良好,跟 cgi 的合做也很愉快。直到有一天,咱們的網站遭受了
CSRF攻擊。解決 CSRF攻擊最簡單的一個辦法就是在 HTTP請求中帶上一個 Token 參數。
.

var getToken = function(){
return 'Token';
}


如今的任務是給每一個 ajax請求都加上 Token 參數:
var ajax = function( type, url, param ){
param = param || {};
Param.Token = getToken(); // 發送 ajax 請求的代碼略...
};

雖然已經解決了問題,但咱們的 ajax 函數相對變得僵硬了,每一個從 ajax 函數裏發出的請求
都自動帶上了 Token 參數,雖然在如今的項目中沒有什麼問題,但若是未來把這個函數移植到其
他項目上,或者把它放到一個開源庫中供其餘人使用, Token 參數都將是多餘的。

也許另外一個項目不須要驗證 Token ,或者是 Token 的生成方式不一樣,不管是哪一種狀況,都必
須從新修改 ajax 函數。

爲了解決這個問題,先把 ajax 函數還原成一個乾淨的函數:

var ajax= function( type, url, param ){
console.log(param); // 發送 ajax 請求的代碼略
};
而後把 Token 參數經過 Function.prototyte.before 裝飾到 ajax 函數的參數 param 對象中:
var getToken = function(){
return 'Token';
}
ajax = ajax.before(function( type, url, param ){
param.Token = getToken();
});
ajax( 'get', 'http:// xxx.com/userinfo', { name: 'sven' } );
從 ajax 函數打印的 log能夠看到, Token 參數已經被附加到了 ajax 請求的參數中:
{name: "sven", Token: "Token"}

明顯能夠看到,用 AOP 的方式給 ajax 函數動態裝飾上 Token 參數,保證了 ajax 函數是一
個相對純淨的函數,提升了 ajax 函數的可複用性,它在被遷往其餘項目的時候,不須要作任何
修改。

6.插件式的表單驗證

咱們不少人都寫過許多表單驗證的代碼,在一個 Web 項目中,可能存在很是多的表單,如
註冊、登陸、修改用戶信息等。在表單數據提交給後臺以前,經常要作一些校驗,好比登陸的時
候須要驗證用戶名和密碼是否爲空,代碼以下:

用戶名:<input id="username" type="text"/>
密碼: <input id="password" type="password"/>
<input id="submitBtn" type="button" value="提交">


var username = document.getElementById( 'username' ),
password = document.getElementById( 'password' ),
submitBtn = document.getElementById( 'submitBtn' );
var formSubmit = function(){
if ( username.value === '' ){
return alert ( '用戶名不能爲空' );
}
if ( password.value === '' ){
return alert ( '密碼不能爲空' );
}
var param = {
username: username.value,
password: password.value
}
ajax( 'http:// xxx.com/login', param ); // ajax 具體實現略
}
submitBtn.onclick = function(){
formSubmit();
}

formSubmit 函數在此處承擔了兩個職責,除了提交 ajax請求以外,還要驗證用戶輸入的合法
性。這種代碼一來會形成函數臃腫,職責混亂,二來談不上任何可複用性。
本節的目的是分離校驗輸入和提交 ajax 請求的代碼,咱們把校驗輸入的邏輯放到 validata
函數中,而且約定當 validata 函數返回 false 的時候,表示校驗未經過,代碼以下:

var validata = function(){
if ( username.value === '' ){
alert ( '用戶名不能爲空' );
return false;
}
if ( password.value === '' ){
alert ( '密碼不能爲空' );
return false;
}
}

var formSubmit = function(){
if ( validata() === false ){ // 校驗未經過
return;
}
var param = {
username: username.value,
password: password.value
}    
ajax( 'http:// xxx.com/login', param );
}
submitBtn.onclick = function(){
formSubmit();
}

如今的代碼已經有了一些改進,咱們把校驗的邏輯都放到了 validata 函數中,但 formSubmit
函數的內部還要計算 validata 函數的返回值,由於返回值的結果代表了是否經過校驗。

接下來進一步優化這段代碼,使 validata 和 formSubmit 徹底分離開來。首先要改寫 Function.
prototype.before , 若是 beforefn 的執行結果返回 false ,表示再也不執行後面的原函數,代碼以下:

Function.prototype.before = function( beforefn ){
var __self = this;
return function(){
if ( beforefn.apply( this, arguments ) === false ){
// beforefn 返回 false 的狀況直接 return,再也不執行後面的原函數
return;
}
return __self.apply( this, arguments );
}
}


var validata = function(){
if ( username.value === '' ){
alert ( '用戶名不能爲空' );
return false;
}
if ( password.value === '' ){
alert ( '密碼不能爲空' );
return false;
}
}


formSubmit = formSubmit.before( validata );
submitBtn.onclick = function(){
formSubmit();
}

在這段代碼中,校驗輸入和提交表單的代碼徹底分離開來,它們再也不有任何耦合關係,
formSubmit = formSubmit.before( validata ) 這句代碼,如同把校驗規則動態接在 formSubmit 函數
以前, validata 成爲一個即插即用的函數,它甚至能夠被寫成配置文件的形式,這有利於咱們分
開維護這兩個函數。再利用策略模式稍加改造,咱們就能夠把這些校驗規則都寫成插件的形式,
用在不一樣的項目當中。

var func = function(){
alert( 1 );
}
func.a = 'a';
func = func.after( function(){
alert( 2 );
});
alert ( func.a ); // 輸出:undefined

另外,這種裝飾方式也疊加了函數的做用域,若是裝飾的鏈條過長,性能上也會受到一些
影響。

7 裝飾者模式和代理模式

裝飾者模式和
代理模式的結構看起來很是相像,這兩種模式都描述了怎樣爲對象提供
必定程度上的間接引用,它們的實現部分都保留了對另一個對象的引用,而且向那個對象發送
請求。
代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直
接訪問本體不方便或者不符合須要時,爲這個本體提供一個替代者。本體定義了關鍵功能,而代
理提供或拒絕對它的訪問,或者在訪問本體以前作一些額外的事情。裝飾者模式的做用就是爲對
象動態加入行爲。換句話說,代理模式強調一種關係(Proxy與它的實體之間的關係),這種關係
能夠靜態的表達,也就是說,這種關係在一開始就能夠被肯定。而裝飾者模式用於一開始不能確
定對象的所有功能時。代理模式一般只有一層代理本體的引用,而裝飾者模式常常會造成一條
長長的裝飾鏈。
在虛擬代理實現圖片預加載的例子中,本體負責設置 img 節點的 src,代理則提供了預加載
的功能,這看起來也是「加入行爲」的一種方式,但這種加入行爲的方式和裝飾者模式的偏重點
是不同的。裝飾者模式是實實在在的爲對象增長新的職責和行爲,而代理作的事情仍是跟本體
同樣,最終都是設置 src。但代理能夠加入一些「聰明」的功能,好比在圖片真正加載好以前,
先使用一張佔位的 loading圖片反饋給客戶。

小結

經過數據上報、統計函數的執行時間、動態改變函數參數以及插件式的表單驗證這 4個 例子,咱們瞭解了裝飾函數,它是 JavaScript中獨特的裝飾者模式。這種模式在實際開發中很是 有用,除了上面提到的例子,它在框架開發中也十分有用。做爲框架做者,咱們但願框架裏的函 數提供的是一些穩定而方便移植的功能,那些個性化的功能能夠在框架以外動態裝飾上去,這可 以免爲了讓框架擁有更多的功能,而去使用一些 if 、 else 語句預測用戶的實際須要。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息