在Javascript中使用Reflection與Proxy Pattern實作AOP

 Wed, 18 Nov 2009 14:04:13 +0800

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

(這一篇原本是發表在ithelp第二屆鐵人賽的文章,我覺得比較有放到blog的價值,所以也把他貼過來。)