Spring 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,只是還沒仔細研究是否可行。