在Javascript中使用Reflection與Proxy Pattern實作AOP
Wed, 18 Nov 2009 14:04:13 +0800Spring Framework有兩個主要的功能,就是IoC容器與AOP。有接觸過AOP的人,就會知道AOP有許多好處,主要是有許多程式的需求其實跟程式的邏輯沒有直接關係,但是又需要在適時插入到邏輯中,最常見的就是在程式中插入紀錄log的功能。像這類的需求可以把它稱作Aspect或是cross-cutting concern,AOP(Aspect Oriented Programming)主要的目的,就是集中處理這一類的需求,讓這一類的需求與邏輯可以拆開,但是又可以在適當的時機介入到程式邏輯中。
AOP可以通過Proxy Pattern來實作,也就是說,透過一個Proxy來執行程式,因為是透過Proxy,所以有機會在做執行動作的前後做介入。Proxy有兩種,一種是靜態Proxy,它必須為每個要透過Proxy執行的類別設計一個相同介面的Proxy類別,然後透過這個Proxy來執行這個類別,這樣在實作上非常麻煩。自從Java有了Reflection能力後,可以用他來偵測類別的介面然後動態地產生Proxy來執行類別,這樣可以節省很大的功夫。
Javascript具備有基本的Reflection功能,所以也有可能用這個功能來實現動態Proxy,然後用來實作AOP。
結構
Javascript可以利用for...in語法,來列舉出一個物件所擁有的properties。像這樣:
function a() { this.p1 = "p1"; this.showP1 = function() { alert(this.p1); }; } var b = new a(); var str = ''; for (var i in b) { str += '[' + i + "]\n"; } alert(str);
同時,Javascript可以用[]來存取物件的Properties。像這樣:
function a() { this.p1 = "p1"; this.showP1 = function() { alert(this.p1); }; } var b = new a(); alert(b['p1']); b['showP1']();
透過這兩種語法,Javascript就可以有Reflection的能力。不過這有個限制,一些內建的物件,他的屬性可能會是[[DontEnum]],也就是不可列舉的,而自訂的物件在下一版的ECMA-262中,也可以這樣設定他的屬性。如此一來可能會讓一些Properties不會被列舉出來。所以要能完整支援Reflection的物件,就不能額外設定這個property的屬性。
底下就實作一個動態Proxy物件,並讓它可以透過constructor來傳入被代理的物件,並設定要在被代理物件的函數執行前、執行後、以及包裹在所執行函數外的函數。除了使用Reflection,為了方便把函數包裹起來,我從google的base.js裡面偷來了一個函數叫做bind,它可以在函數執行前先把參數指派給它,這樣到了要實際執行時,我只要用函數的參考後面加上()就可以正確執行。下一版的ECMA-262,也會把這個函數加入到Function.prototype中,所以Javascript的函數都可以先把他的參數用這個方法先bind上去。
(function(){ var bind = function(fn, selfObj, var_args) { var boundArgs = fn.boundArgs_; if (arguments.length > 2) { var args = Array.prototype.slice.call(arguments, 2); if (boundArgs) { args.unshift.apply(args, boundArgs); } boundArgs = args; } selfObj = fn.boundSelf_ || selfObj; fn = fn.boundFn_ || fn; var newfn; var context = selfObj || goog.global; if (boundArgs) { newfn = function() { var args = Array.prototype.slice.call(arguments); args.unshift.apply(args, boundArgs); return fn.apply(context, args); }; } else { newfn = function() { return fn.apply(context, arguments); }; } newfn.boundArgs_ = boundArgs; newfn.boundSelf_ = selfObj; newfn.boundFn_ = fn; return newfn; }; AOP = function(o, c) { var methods = []; var before = function(){}; var after = function(){}; var arround = function(f){f();}; if (c.before && typeof c.before === 'function') before = c.before; if (c.after && typeof c.after === 'function') after = c.after; if (c.arround && typeof c.arround === 'function') arround = c.arround; if (typeof o == 'function') { var o = new o(); } for (var i in o) { if (typeof o[i] == 'function') { methods.push(i); } } i = 0; for (; i<methods.length; i++) { this[methods[i]] = function(methodName) { return function() { before(methodName); arround( bind( o[methodName], o, Array.prototype.slice.call(arguments) ) , methodName); after(methodName); }; }(methods[i]); } }; })();
接下來可以實驗一下:
function a() { this.show=function(msg) { alert('a.show: ' + msg); }; } var b = new a(); b.show('hey');
執行這個範例中的b.show('hey'),就會顯示'a.show: hey'。接下來我們用上面實作的AOP類別來包裹b,產生物件變數c,並且加入在他的函數執行前後,及包裹了函數並在執行前後顯示訊息的程式:
var c = new AOP(b, { before: function(m){alert('before method: '+m);}, after: function(m){alert('after method: '+m);}, arround: function(f, m){ alert('before within arround method: '+m); f(); alert('after within arround method: '+m); } }); c.show('hey');
這樣就會依序顯示訊息:'before method: show'、'before within arround method: show'、'a.show: hey'、'after within arround method: show'、'after method: show'。
上例可以透過http://jsbin.com/icuxi來試用。
應用
範例裡面只是一個非常粗糙的實作,功能也有限。要看別人怎麼做、怎麼用,會更有價值。
我上網找了一下,YUI3這個Javascript框架裡面,有在Custom Event架構中實作AOP,也許可以拿來參考。另外,如果我自己要拿來用的話,也許可以在做單元測試的時候,用它來產生mock object,只是還沒仔細研究是否可行。
(這一篇原本是發表在ithelp第二屆鐵人賽的文章,我覺得比較有放到blog的價值,所以也把他貼過來。)