[1]概念 [2]javascript裝飾者 [3]裝飾函數 [4]AOP [5]AOP應用實例 [6]裝飾者模式和代理模式 ...
前面的話
在程式開發中,許多時候都並不希望某個類天生就非常龐大,一次性包含許多職責。那麼可以使用裝飾者模式。裝飾者模式可以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象。本文將詳細介紹裝飾者模式
概念
在傳統的面向對象語言中,給對象添加功能常常使用繼承的方式,但是繼承的方式並不靈活,還會帶來許多問題:一方面會導致超類和子類之間存在強耦合性,當超類改變時,子類也會隨之改變;另一方面,繼承這種功能復用方式通常被稱為“白箱復用”,“白箱”是相對可見性而言的,在繼承方式中,超類的內部細節是對子類可見的,繼承常常被認為破壞了封裝性
使用繼承還會帶來另外一個問題,在完成一些功能復用的同時,有可能創建出大量的子類,使子類的數量呈爆炸性增長。比如現在有4種型號的自行車,為每種自行車都定義了一個單獨的類。現在要給每種自行車都裝上前燈、尾燈和鈴鐺這3種配件。如果使用繼承的方式來給每種自行車創建子類,則需要4×3=12個子類。但是如果把前燈、尾燈、鈴鐺這些對象動態組合到自行車上面,則只需要額外增加3個類
這種給對象動態地增加職責的方式稱為裝飾者(decorator)模式。裝飾者模式能夠在不改變對象自身的基礎上,在程式運行期間給對象動態地添加職責。跟繼承相比,裝飾者是一種更輕便靈活的做法,這是一種“即用即付”的方式,比如天冷了就多穿一件外套,需要飛行時就在頭上插一支竹蜻蜓
作為一門解釋執行的語言,給javascript中的對象動態添加或者改變職責是一件再簡單不過的事情,雖然這種做法改動了對象自身,跟傳統定義中的裝飾者模式並不一樣,但這無疑更符合javascript的語言特色。代碼如下:
var obj ={ name:'match', address:'北京' }; obj.address= obj.address + '平谷區';
傳統面向對象語言中的裝飾者模式在javascript中適用的場景並不多,如上面代碼所示,通常並不太介意改動對象自身
假設在編寫一個飛機大戰的游戲,隨著經驗值的增加,操作的飛機對象可以升級成更厲害的飛機,一開始這些飛機只能發射普通的子彈,升到第二級時可以發射導彈,升到第三級時可以發射原子彈
下麵來看代碼實現,首先是原始的飛機類:
var Plane = function(){}; Plane.prototype.fire = function(){ console.log( '發射普通子彈' ); }
接下來增加兩個裝飾類,分別是導彈和原子彈:
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方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操作,隨後把請求轉發給鏈中的下一個對象
因為裝飾者對象和它所裝飾的對象擁有一致的介面,所以它們對使用該對象的客戶來說是透明的,被裝飾的對象也並不需要瞭解它曾經被裝飾過,這種透明性使得可以遞歸地嵌套任意多個裝飾者對象
在《設計模式》成書之前,GoF原想把裝飾者(decorator)模式稱為包裝器(wrapper)模式。從功能上而言,decorator能很好地描述這個模式,但從結構上看,wrapper的說法更加貼切。裝飾者模式將一個對象嵌入另一個對象之中,實際上相當於這個對象被另一個對象包裝起來,形成一條包裝鏈。請求隨著這條鏈依次傳遞到所有的對象,每個對象都有處理這條請求的機會
javascript裝飾者
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(); // 分別輸出: 發射普通子彈、發射導彈、發射原子彈
裝飾函數
在javascript中可以很方便地給某個對象擴展屬性和方法,但卻很難在不改動某個函數源代碼的情況下,給該函數添加一些額外的功能。在代碼的運行期間,很難切入某個函數的執行環境。要想為函數添加一些功能,最簡單粗暴的方式就是直接改寫該函數,但這是最差的辦法,直接違反了開放——封閉原則
var a = function(){ alert(1); } //改成: var a = function(){ alert(1); alert(2); }
很多時候不想去碰原函數,也許原函數是由其他同事編寫的,裡面的實現非常雜亂。現在需要一個辦法,在不改變函數源代碼的情況下,能給函數增加功能,通過保存原引用的方式就可以改寫某個函數:
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); }
這樣的代碼當然是符合開放——封閉原則的,在增加新功能的時候,確實沒有修改原來的window.onload代碼,但是這種方式存在以下兩個問題
1、必須維護_onload這個中間變數,雖然看起來並不起眼,但如果函數的裝飾鏈較長,或者需要裝飾的函數變多,這些中間變數的數量也會越來越多
2、遇到了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:
<button id="button"></button> <script> var _getElementById = document.getElementById; document.getElementById=function(){ alert(1); return _getElementById.apply(document,arguments); } var button = document.getElementById('button'); </script>
但這樣做顯然很不方便
AOP
下麵使用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一模一樣,唯一不同的地方在於讓新添加的函數在原函數執行之後再執行
下麵是一個例子
<button id="button"></button> <script> 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 ); </script>
再回到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();
AOP應用實例
用AOP裝飾函數的技巧在實際開發中非常有用。不論是業務代碼的編寫,還是在框架層面,都可以把行為依照職責分成粒度更細的函數,隨後通過裝飾把它們合併到一起,這有助於編寫一個松耦合和高復用性的系統
【數據統計上報】
分離業務代碼和數據統計代碼,無論在什麼語言中,都是AOP的經典應用之一。在項目開發的結尾階段難免要加上很多統計數據的代碼,這些過程可能讓我們被迫改動早已封裝好的函數。比如頁面中有一個登錄button,點擊這個button會彈出登錄浮層,與此同時要進行數據上報,來統計有多少用戶點擊了這個登錄button
<html> <button tag="login" id="button">點擊打開登錄浮層</button> <script> var showLogin = function(){ console.log( '打開登錄浮層' ); log( this.getAttribute( 'tag' ) ); } var log = function( tag ){ console.log( '上報標簽為: ' + tag ); // (new Image).src = 'http://xx.com/report?tag=' + tag; // 真正的上報代碼略 } document.getElementById( 'button' ).onclick = showLogin; </script> </html>
在showLogin函數里,既要負責打開登錄浮層,又要負責數據上報,這是兩個層面的功能,在此處卻被耦合在一個函數里。使用AOP分離之後,代碼如下:
<html> <button tag="login" id="button">點擊打開登錄浮層</button> <script> 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; </script> </html>
【用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 =f unction(type,url,param){ console.dir(param); //發送ajax請求的代碼略 }; ajax('get','http://xx.com/userinfo',{name:'match'});
上面的偽代碼表示向後臺cgi發起一個請求來獲取用戶信息,傳遞給cgi的參數是{name:'match'}。ajax函數在項目中一直運轉良好,跟cgi的合作也很愉快。直到有一天,網站遭受了CSRF攻擊。解決CSRF攻擊最簡單的一個辦法就是在HTTP請求中帶上一個Token參數。假設已經有一個用於生成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://xx.com/userinfo',{name:'match'});
從ajax函數列印的log可以看到,Token參數已經被附加到了ajax請求的參數中:
{name:"match",Token:"Token"}
明顯可以看到,用AOP的方式給ajax函數動態裝飾上Token參數,保證了ajax函數是一個相對純凈的函數,提高了ajax函數的可復用性,它在被遷往其他項目的時候,不需要做任何修改
【插件式表單驗證】
在一個Web項目中,可能存在非常多的表單,如註冊、登錄、修改用戶信息等。在表單數據提交給後臺之前,常常要做一些校驗,比如登錄的時候需要驗證用戶名和密碼是否為空,代碼如下:
<body> 用戶名:<input id="username" type="text"/> 密碼: <input id="password" type="password"/> <input id="submitBtn" type="button" value="提交"></button> <script> 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://xx.com/login', param ); // ajax 具體實現略 } submitBtn.onclick = function(){ formSubmit(); } </script>
</body>
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; } } var formSubmit = function(){ var param = { username: username.value, password: password.value } ajax( 'http://xx.com/login', param ); } formSubmit = formSubmit.before( validata ); submitBtn.onclick = function(){ formSubmit(); }
在這段代碼中,校驗輸入和提交表單的代碼完全分離開來,它們不再有任何耦合關係,formSubmit=formSubmit.before(validata)這句代碼,如同把校驗規則動態接在formSubmit函數之前,validata成為一個即插即用的函數,它甚至可以被寫成配置文件的形式,這有利於分開維護這兩個函數。再利用策略模式稍加改造,就可以把這些校驗規則都寫成插件的形式,用在不同的項目當中
值得註意的是,因為函數通過Function.prototype.before或者Function.prototype.after被裝飾之後,返回的實際上是一個新的函數,如果在原函數上保存了一些屬性,那麼這些屬性會丟失。代碼如下:
var func = function(){ alert(1); } func.a='a'; func=func.after(function(){ alert(2); }); alert(func.a); //輸出:undefined
另外,這種裝飾方式也疊加了函數的作用域,如果裝飾的鏈條過長,性能上也會受到一些影響
裝飾者模式和代理模式
裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣為對象提供一定程度上的間接引用,它們的實現部分都保留了對另外一個對象的引用,並且向那個對象發送請求。代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是為對象動態加入行為。換句話說,代理模式強調一種關係(Proxy與它的實體之間的關係),這種關係可以靜態的表達,也就是說,這種關係在一開始就可以被確定。而裝飾者模式用於一開始不能確定對象的全部功能時。代理模式通常只有一層代理——本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈