2017年11月20日 星期一

MS Enterprise Library 6.0 (五) - 再談AOP, 如何實作?

這系列的上一篇居然是2013年…沒說錯,是2013,四年前!!!

MS Enterprise Library 6.0(一) - Unity Application Block
MS Enterprise Library 6.0 (二) - Logging Application Block
MS Enterprise Library 6.0 (三) - Exception Handling Application Block
MS Enterprise Library 6.0 (四) - Policy Injection Application Block

說真的,如果不是因為開了課,我根本早忘記我曾經寫過上面這幾篇文章。

上週六,是『團隊開發 與 架構設計 實務 』這個課程的第二天上課,在課程中我們談到了一個框架設計上我常用的技巧,也就是如何透過Attribute來實作AOP(嚴格說起來是廣義的AOP)。如果你不確定明白AOP(Aspect-Oriented Programming)的基本概念,可以參考四年前的這篇。(天啊,四年前…)

課程中,我做了一個範例(具體內容後面說),和某位學員討論的時候,突然看到學員的螢幕上出現了一篇看似有點眼熟的文章,語氣跟我好像…咦? 作者怎麼是我?是了,就是剛才說的四年前寫的那篇這篇文章中介紹了PIAB(policy injection application block),這是當年我在Tech Days介紹MS Enterprise Library時,特別喜愛的一個application block,它很精采的把Business Logic和Infrastructure Logic做一個非常有效的切割,達成cross-cutting concerns的獨立性。

這一系列的文章或許是因為點擊率不很高,也或者是當時有別的事情再忙…我寫著寫著居然就忘了…也因此這個系列就沒了下文。時過境遷,這次上課的時候,我也不再介紹PIAB,因為這個PIAB Library的nuget套件很不爭氣的始終停留在2013年都沒更新,因此我們只能從底層架構說明透過Attribute實現AOP概念的作法。

不過這樣也好,學員可以從根本理解實現AOP可能的幾個方式 。然而,美中不足的是這樣去實現AOP的Method呼叫時,就得要有另一個Loader/Lancher來做attribute行為上的判斷。

『這樣呼叫Method時似乎有些不直覺?有沒有更好的做法?』學員問。

『其實你可以自己仿Unity Container做一個Proxy的機制…』因為在課堂上我們還有一併介紹到Unity,因此我這麼回答。

然而因為時間的關係,我也就無法在課堂上直接做一個Demo,想想頗為遺憾,因此課程結束後的這幾天,我抽了點時間把這個心裡想要實現的小套件,仿造PIAB的方式來製作一下。

需求與命題

首先來整理一下這個命題,我們知道AOP想把Business Logic和Infrastructure Logic做一個切割,達成cross-cutting concerns的獨立性,具體情境是什麼呢?看底下這一段code:

上面這段程式碼的第10行使用了Calculate()這個Method來計算BMI,但這段Code有些小問題,例如,傳入身高體重是0時候,可能發生Exception。

你可能會想,這不要緊,我們在Method中加入try…catch,類似底下這樣(當然我知道可以加上傳入參數的檢查,但我想講的點是try…catch啦,所以我們先這樣幹):

這樣做沒問題,但如此一來,我們就讓計算BMI這段code,嚴重的與處理exception中的發mail或發Line訊息的功能相依!

此外,倘若我們想針對該Method被呼叫幾次,做一個Log,你可能也會在Calculate()這個Method中加入讀寫檔案存取log或db的code,這下不得了,簡單的BMI計算(business logic)開始與檔案系統、DB、發mail…etc(Infrastructure  logic)相依,萬一你程式碼中不只一個地方需要進行Log,而是有好幾個method都需要log,那就更精采了…

而AOP希望實現的,就是讓這些Business Logic(BMI計算)和Infrastructure Logic(Log, 錯誤處理, …etc.)之間可以不要彼此過分相依…

如何實現呢?可以透過Attribute。

想像一下,倘若我們可以這樣做:

倘若我們可以透過對Method掛上我們寫好的Attribute,就賦予Calculate()這個Method特殊的功能,例如一旦發生exception,就自動發Line訊息通知Admin,如果該Method被呼叫,就自動在Log file中記錄…。倘若這一切只需要掛上Attribute就能實現,我們就毋須破壞原本計算BMI的Calculate()這個method中的程式碼了…

但,真的可以這樣嗎?
當然可以…

請在專案中透過nuget加入一個套件 isRock.Framework.AOP,完成後,請先引用using isRock.Framework.AOP:

接著設計一個Logging Attribute(請注意繼承自PolicyInjectionAttributeBase):

我們在上面這個自行設計的Attribute當中,透過SaveLog這個方法來儲存Log。並且override了AfterInvoke()來呼叫SaveLog()。

完成後再將原本計算 BMI的Class修改如下(請注意只改了1, 13行):

最後,再調整一下Main中的程式碼(只改了第5行)。

神奇的事情發生了。

你會發現,我們的主程式,上面的main()中其實只改了小小一行(第五行),就讓我們自己寫的Logging attribute作用到Calculate()的身上了。由於Logging attribute中的第6行override了AfterInvoke()這個method,該Method會自動在Calculate()方法被呼叫後自動被執行,其中的SaveLog()程式碼是開檔寫Log…

如此一來,每當Calculate()方法被呼叫,log.txt檔案就會被開啟並被加入一列,變成:

這樣就實現了我們一開始的需求,當某一個Method被掛上Attribute之後,就能夠增加原本沒有的功能。且將Business Logic和Infrastructure Logic做一個切割,達成cross-cutting concerns的獨立性。

我們回憶一下我們做了什麼?

  1. 首先我們引入了Nuget套件 isRock.Framework
  2. Using了isRock.Framework.AOP
  3. 接著設計了一個自己的Logging Attribute(繼承自PolicyInjectionAttributeBase)
  4. 在上述Logging Attribute當中,override了AfterInvoke()方法,該方法會在目標Method(被掛上attribute的Method)被叫用之後自動被呼叫
  5. 在上述AfterInvoke()方法中呼叫SaveLog()開檔並加上『目標Method被呼叫』的Log
  6. 為了讓attribute生效,我們修改原始的BMIProcessor類別,在Calculate()方法上掛上LoggingAttribute:
    [Logging(LogFileName ="log.txt")]
  7. 並且讓BMIProcessor類別繼承自PolicyInjectionComponentBase
  8. 調整主程式為
    BMIProcessor BMI = PolicyInjection.Create<BMIProcessor>();

就這樣,我們透過這個框架,可以讓開發人員輕鬆的設計自己的Attribute,並且將attribute掛在Method上,實現Business Logic和Infrastructure Logic分離的AOP情境。

骨子裡,他還是使用PIAB的作法,透過reflection與Proxy的手法,完成以attribute實現類似AOP的功能:

如果你深入看,會發現我們的PolicyInjectionAttributeBase已經實做了BeforeInvoke、AfterInvoke、OnException三種Method,可讓開發人員輕易的實現自己想實現的Attribute。

嚴格說起來,我們用PIAB這樣的手法雖然可以實現部份AOP的目標,但還夠不全面,這也是PIAB不稱自己為AOP框架的原因。但是,即便只是能實現到目前這樣,也已經很符合我們在日常開發工作當中使用了。

這篇先寫到這邊,改天我們來談談如何實現OnException的Attribute。

完整程式碼可以參考:
https://github.com/isdaviddong/isRock.Framework.AOP-Examples

參考資料:
Introduction to the Policy Injection Application Block

-----------------
相關教育訓練: http://www.studyhost.tw/NewCourses/Architecture
若這篇文章對您有所幫助,請點選這裡加入FaceBook專頁按讚並追蹤,也歡迎您幫我們分享出去,謝謝您的支持。

沒有留言: