Javascript中的AOP程式設計

qingbob.com發表於2014-09-03

Duck punch

我們先不談AOP程式設計,先從duck punch程式設計談起。

如果你去wikipedia中查詢duck punch,你查閱到的應該是monkey patch這個詞條。根據解釋,Monkey patch這個詞來源於 guerrilla patch,意為在執行中悄悄的改變程式碼,而guerrilla 這個詞與 gorilla 同音,而後者意又與monkey相近(前者為“猩猩”的意思),最後就演變為了monkey patch。

如果你沒有聽說過duck punch,但你或許聽說過duck typing。舉一個通俗的例子,如何辨別一隻鴨子:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

沒錯,如果我發現有一類動物像鴨子一樣叫,像鴨子一樣游泳,那麼它就是一隻鴨子!

duck

這個檢測看上去似乎有一些理所當然和無厘頭,但卻非常的實用。 並且在程式設計中可以用來解決一類問題——對於Javascript或者類似的動態語言,如何實現“介面”或者“基類”呢?我們可以完全不用在乎它們的過去如何,我們只關係在使用它們的時候,方法的型別或者引數是否是我們需要的:

var quack = someObject.quack;

if (typeof quack == “function” && quck.length == arguLength)
{
// This thing can quack
}

扯遠了,其實我想表達的是duck punch其實是由duck typing演化而來的:

if it walks like a duck and talks like a duck, it’s a duck, right? So if this duck is not giving you the noise that you want, you’ve got to just punch that duck until it returns what you expect.

當你想一隻鴨子發出驢的叫聲怎麼辦,揍到它發出驢的叫聲為止……話說這讓我想到一個非常形象的笑話:

為了測試美國、香港、中國大陸三地警察的實力, 聯合國將三隻兔子放在三個森林中,看三地警察誰先找出兔子。任務:找出兔子。 (中間省略……) 最後是某國警察,只有四個,先打了一天麻將,黃昏時一人拿一警棍進入森林,沒五分鐘,聽到森林裡傳來一陣動物的慘叫,某國警察一人抽著一根菸有說有笑的出來,後面拖著一隻鼻青臉腫的熊,熊奄奄一息的說到:“不要再打了,我就是兔子……”

雖然duck punch有些暴力,但不失為一個有效的方法。落實到程式碼上來說就是讓原有的程式碼相容我們需要的功能。比如Paul Irish部落格上的這個例子:

/**
我們都知道jQuery的`$.css`方法可以通過使用顏色的名稱給元素進行顏色賦值。
但jQuery內建的顏色並非是那麼豐富,如果我們想新增我們自定義的顏色名稱應該怎麼辦?比如我們想新增`Burnt Sienna`這個顏色
*/

(function($){

// 把原方法暫存起來:
var _oldcss = $.fn.css;

// 重寫原方法:
$.fn.css = function(prop,value){

// 把自定義的顏色寫進分支判斷裡,特殊情況特殊處理
if (/^background-?color$/i.test(prop) && value.toLowerCase() === ‘burnt sienna’) {
return _oldcss.call(this,prop,’#EA7E5D’);

// 一般情況一般處理,呼叫原方法
} else {
return _oldcss.apply(this,arguments);
}
};
})(jQuery);

// 使用方法:
jQuery(document.body).css(‘backgroundColor’,'burnt sienna’)

同時可以推倒出duck punch的模式不過如此:

(function($){

var _old = $.fn.method;

$.fn.method = function(arg1,arg2){

if ( … condition … ) {
return  ….
} else {           // do the default
return _old.apply(this,arguments);
}
};
})(jQuery);

但是這麼做有一個問題:需要修改原方法。這違背了“開放-封閉”原則,本應對擴充開放,對修改關閉。怎麼解決這個問題呢?使用AOP程式設計。

AOP

入門

AOP全稱為Aspect-oriented programming,很明顯這是相對於Object-oriented programming而言。Aspect可以翻譯為“切面”或者“側面”,所以AOP也就是面向切面程式設計。

怎麼理解切面?

在物件導向程式設計中,我們定義的類通常是領域模型,它的擁有的方法通常是和純粹的業務邏輯相關。比如:

Class Person
{
private int money;
public void pay(int price)
{
this.money = this.money – price;
}
}

但通常實際情況會更復雜,比如我們需要在付款的pay方法中加入授權檢測,或者用於統計的日誌傳送,甚至容錯程式碼。於是程式碼會變成這樣:

Class Person
{
private int money
public void pay(price)
{
try
{
if (checkAuthorize() == true) {
this.money = this.money – price;
sendLog();
}
}
catch (Exception e)
{

}
}
}

更可怕的是,其他的方法中也要新增相似的程式碼,這樣以來程式碼的可維護性和可讀性便成了很大的問題。我們希望把這些零散但是公共的非業務程式碼收集起來,更友好的使用和管理他們,這便是切面程式設計。切面程式設計在避免修改遠程式碼的基礎上實現了程式碼的複用。就好比把不同的物件橫向剖開,關注於內部方法改造。而物件導向程式設計更關注的是整體的架構設計。

實現

在上一節中介紹的duck punch與切面程式設計類似,都是在改造原方法的同時保證原方法功能。但就像結尾說的一樣,直接修改原方法的模式有悖於物件導向最佳實踐的原則。

Javascript可以採用裝飾者模式(給原物件新增額外的職責但避免修改原物件)實現AOP程式設計。注意在這裡強調的是實現,我進一步想強調的是,切面程式設計只是一種思想,而裝飾者模式只是實踐這種思想的一種手段而已,比如在Java中又可以採用代理模式等。切面程式設計在Java中發揮的餘地更多,也更標準,本想把Java的實現模式也搬來這篇文章中,但不才Java水平有限,對Java的實現不是非常理解。在這裡就只展示Javascript的實現。

AOP中有一些概念需要介紹一下,雖然我們不一定要嚴格執行

  • joint-point:原業務方法;
  • advice:攔截方式
  • point-cut:攔截方法

關於這三個概念我們可以串起來可以這麼理解:

當我們使用AOP改造一個原業務方法(joint-point)時,比如加入日誌傳送功能(point-cut),我們要考慮在什麼情況下(advice)傳送日誌,是在業務方法觸發之前還是之後;還是在丟擲異常的時候,還是由日誌傳送是否成功再決定是否執行業務方法。

比如gihub上的meld這個開源專案,就是一個很典型的AOP類庫,我們看看它的API:

// 假設我們有一個物件myObject, 並且該物件有一個doSomething方法:

var myObject = {
doSomething: function(a, b) {
return a + b;
}
};

// 現在我們想擴充它,在執行那個方法之後列印出剛剛執行的結果:

var remover = meld.after(myObject, ‘doSomething’, function(result) {
console.log(‘myObject.doSomething returned: ‘ + result);
});

// 試試執行看:

myObject.doSomething(1, 2); // Logs: “myObject.doSomething returned: 3″

// 這個時候我們想移除剛剛的修改:

remover.remove();

由此可以看出,AOP介面通常需要三個引數,被修改的物件,被修改物件的方法(joint-point),以及觸發的時機(adivce),還有觸發的動作(point-cut)。上面說了那麼多的概念,現在可能要讓各位失望了,Javascript的實現原理其實非常簡單

function doAfter(target, method, afterFunc){
var func = target[method];
return function(){
var res = func.apply(this, arguments);
afterFunc.apply(this, arguments);
return res;
};
}

當然,如果想看到更完備的解決方案和程式碼可以參考上面所說的meld專案

結束語

這一篇一定讓你失望了,程式碼簡單又寥寥無幾。本篇主要在於介紹有關duck和AOP的這幾類思想,我想程式設計的樂趣不僅僅在於落實在編碼上,更在於整個架構的設計。提高程式碼的可維護性和可擴充性會比高深莫測的程式碼更重要。

參考文獻:

相關文章