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.
『沒錯,如果我發現有一類動物像鴨子一樣叫,像鴨子一樣游泳,那麼它就是一隻鴨子!
這個檢測看起來似乎有一些理所當然和無厘頭,但卻非常的實用。 並且在程式設計中可以用來解決一類問題──對於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 that you want, you've got to just punch that duck until thatit 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的这几类思想,我想编程的乐趣不仅仅在于落实在编码上,更在于整个架构的设计。提高代码的可维护性和可拓展性会比高深莫测的代码更重要。
其实上面
参考文献:
How to Fulfill Your Own Feature Request -or- Duck Punching With jQuery!
Duck Punching JavaScript - Metaprogramming with Prototype
Does JavaScript have the interface type (such as Java’s ‘interface’)?
AOP技术基础