AOP 剖面導向程式-以 Castle Dynamic Proxy 為例

如上篇介紹,這邊使用的是 Castle Dynamic Proxy 這套的 AOP 套件,它所採用的是動態攔截的機制

一般言將程式套上 AOP 最好的方式是調整架構,從設計面去解決,但我想這對大家而言是最可怕的事情

若是你不想動到架構面,卻又急著想要擁有 AOP 的好處,那麼 AOP 工具 可以幫你省下相當多的時間

這邊要介紹的工具可以在 nuget 上搜尋 Castle.Core 即可安裝

以上篇 AOP 剖面導向程式設計初試身手 為例,針對既有的 class service,我們發現 Transaction 是可以被分離出來的功能,並共享使用

    public class OrderService
    {
        public void Update(string orderId)
        {
            using (var tran = new TransactionScope())
            {
                try
                {
                    //update order info
                    tran.Complete();
                }
                catch (Exception ex)
                {

                }
            }
        }

        public void Delete(string orderId)
        {
            using (var tran = new TransactionScope())
            {
                try
                {
                    //delete order info
                    tran.Complete();

                }
                catch (Exception ex)
                {

                }              
            }
        }
    }

 

為既有的類別建立介面

為求方便說明,我另外開了一個 OrderServiceSRP,我把要分離的功能 Transaction 的功能移除掉,並建立其介面,它會長成這樣

裡面的程式碼就變的相當的簡潔了

    public interface IOrderServiceSRP
    {
        void Delete(string orderId);
        void Update(string orderId);
    }
    
    public class OrderServiceSRP : IOrderServiceSRP
    {
        public void Delete(string orderId)
        {
            Console.WriteLine("   Delete Order Info");
        }

        public void Update(string orderId)
        {
            Console.WriteLine("   Update Order Info");
        }
    }

 

新增 Transaction 攔截器

接著我們再建上一個 Transaction 的攔截器,簡言之就是在執行動作前,先做 transaction 的事情

using System;
using System.Transactions;
using Castle.DynamicProxy;

        public void Intercept(IInvocation invocation)
        {           
            using (var tran = new TransactionScope())
            {
                try
                {
                    Console.WriteLine(" Transaction intercept start");

                    invocation.Proceed();

                    Console.WriteLine(" Transaction intercept end");

                    tran.Complete();
                }
                catch (Exception ex)
                {

                }
            }
        }

 

組合類別與攔截器

接著只要在組合根裡面讓 Castle.dynamic.proxy 產生動態攔截的行為即可

            // 1. existing service
            IOrderServiceSRP service = new OrderServiceSRP();

            //2. aspects
            var transactionScopeInterceptor = new AopTransactionInterceptor();

            ////3. runtime decotrator
            var generator = new Castle.DynamicProxy.ProxyGenerator();
            var serviceDecoratee = generator.CreateInterfaceProxyWithTarget<IOrderServiceSRP>(service, transactionScopeInterceptor);

            serviceDecoratee.Update("123");
            serviceDecoratee.Delete("123");

=====output=====

 Transaction intercept start
   Update Order Info
 Transaction intercept end
 Transaction intercept start
   Delete Order Info
 Transaction intercept end

 

若要新增一個 Log 剖面功能怎麼做?

 以單一職責原則,我們不會想再往 transaction 的 AOP 裡面再加上 log 的功能,所以我們一樣新增一個 Log 功能的攔截器

 在這裡並不實作用哪套 Log 工具,僅用 Console.WriteLine() 代表 Log 執行的行為 

 using System;
 using Castle.DynamicProxy;
    public class AopLogInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            //start log here
            try
            {
                Console.WriteLine("Log intercept start");

                invocation.Proceed();

                Console.WriteLine("Log intercept end");
            }
            catch (Exception ex)
            {
                //log here
                throw ex;
            }
        }
    }
 

 

將 Log 功能結合起來

 接下來我們只要做個微調就可以把 Log 的功能套上去了 

           // 1. existing service
            IOrderServiceSRP service = new OrderServiceSRP();

            //2. aspects
            var transactionScopeInterceptor = new AopTransactionInterceptor();
            //add new log aspect 
            var logInterceptor = new AopLogInterceptor();

            ////3. runtime decotrator
            var generator = new Castle.DynamicProxy.ProxyGenerator();

            var serviceDecoratee = generator.CreateInterfaceProxyWithTarget<IOrderServiceSRP>(service, logInterceptor, transactionScopeInterceptor);

            serviceDecoratee.Update("123");
            serviceDecoratee.Delete("123");


 =====output=====

Log intercept start
 Transaction intercept start
   Update Order Info
 Transaction intercept end
Log intercept end
Log intercept start
 Transaction intercept start
   Delete Order Info
 Transaction intercept end
Log intercept end

  在 CreateInterfaceProxyWithTarget 的方法上,可以看到 interceptors 可以是多個的,而不是限定只能用一個

 TInterface CreateInterfaceProxyWithTarget<TInterface>(TInterface target, params IInterceptor[] interceptors)
 
 而它的順序就是你想要套用的順序,比如說 logInterceptor, transactionScopeInterceptor 這樣的套用方式

 那結果就是會先執行 log, 再來是 transaction,最後才是你的 orderService 裡要做的事

 但如果你設定的是  transactionScopeInterceptor,logInterceptor;這麼一來就是相反的執行方式了 

 

 缺點

 1. 效率


  若你去反組譯 Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyWithTarget 若往下看到底層的作法,你會發現這樣的事
 在 Castle Dynamic Proxy 裡做了許多的檢查,比如你傳進來的東西是不是介面,甚至會做實體的檢查是不是實作你的介面的檢查...

如果你的系統有強大效能的要求,諸如此類的檢查可能不是你所希望的,而採用非工具類的做法(如上篇所述),則不會帶來這樣的副作用


 
 2. 如果你有 Transaction 的功能,那麼 Castle.dynamic.proxy 會將該 class 裡的所有 methods 都套上這兩個 AOP 的功能

    OrderService 有 query 的功能理當不意外吧, 那麼 query 資料的功能需要加上 transaction 嗎?我想應該是不用吧

    也許為了這個功能,你會再往下切分為 OrderQueryService,及 OrderUpdateService, OrderDeleteService 或其他的做法

    如此一來也許會導致你的開發時間比你想像還長,影響範圍更大

    下一篇會再介紹後期織入(Compile-Time Weaving)更便利(不想改)的方法
    

結論

 Castle.dynamic.proxy 透過攔截的方式,讓你可以新增攔截器,將你要做的 AOP 功能套上,讓你不需要以太大的幅度修改就有AOP 的功能,相當方便。

 但有些程式碼連介面都沒有的,若是想套用的話就得先從介面先建起;既有程式以效率考量跟套用範圍(以整個 class 做為套用範圍)而言,便利性與彈性仍比不上下一篇要講的後期織入方式的 AOP 工具

 雖說這樣結論好像是鼓勵大家使用工具似的,但最好的方式還是從設計面做起,如果真的遇到太多現實面不可解的困難再考慮是否該使用工具解決