在傳統的面嚮物件語言中,給物件新增功能常常使用繼承的方式,但是繼承的方式並不靈活, 還會帶來許多問題:一方面會導致超類和子類之間存在強耦合性,當超類改變時,子類也會隨之 改變;另一方面,繼承這種功能複用方式通常被稱為“白箱複用”,“白箱”是相對可見性而言的, 在繼承方式中,超類的內部細節是對子類可見的,繼承常常被認為破壞了封裝性。裝飾者模式能夠在不改變物件自身的基礎上,在程式執行期間給物件動態地新增職責。跟繼承相比,裝飾者是一種更輕便靈活的做法,這是一種“即用即付”的方式
故事背景
假設我們在編寫一個飛機大戰的遊戲,隨著經驗值的增加,我們操作的飛機物件可以升級成更厲害的飛機,一開始這些飛機只能發射普通的子彈,升到第二級時可以發射導彈,升到第三級時可以發射原子彈。
程式碼實現(使用裝飾者模式)
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();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈
複製程式碼
使用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;
}
};
複製程式碼
AOP 的應用例項
- 資料統計上報
比如頁面中有一個登入 button,點選這個 button 會彈出登入浮層,與此同時要進行資料上報, 來統計有多少使用者點選了這個登入 button
- 未使用AOP
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; 複製程式碼
- 使用AOP
var showLogin = function(){ console.log( '開啟登入浮層' ); } var log = function(){ console.log( '上報標籤為: ' + this.getAttribute( 'tag' ) ); } showLogin = showLogin.after( log ); // 開啟登入浮層之後上報資料 document.getElementById( 'button' ).onclick = showLogin; 複製程式碼
- 外掛式的表單驗證
我們很多人都寫過許多表單驗證的程式碼,在一個 Web 專案中,可能存在非常多的表單,如 註冊、登入、修改使用者資訊等。在表單資料提交給後臺之前,常常要做一些校驗,比如登入的時 候需要驗證使用者名稱和密碼是否為空
- 未使用AOP
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 ); } submitBtn.onclick = function(){ formSubmit(); } 複製程式碼
- 使用AOP
var validata = function(){ if ( username.value === '' ){ alert ( '使用者名稱不能為空' ); return false; } if ( password.value === '' ){ alert ( '密碼不能為空' ); return false; } } var formSubmit = function(){ var param = { username: username.value, password:password.value } ajax( 'http:// xxx.com/login', param ); } formSubmit = formSubmit.before( validata ); submitBtn.onclick = function(){ formSubmit(); } 複製程式碼
小結
裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣為物件提供 一定程度上的間接引用,它們的實現部分都保留了對另外一個物件的引用,並且向那個物件傳送 請求。 代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是為對 象動態加入行為。