歡迎光臨
每天分享高質量文章

C#中使用面向切麵編程(AOP)中實踐代碼整潔

來自:張蘅水

鏈接:http://www.cnblogs.com/chenug/p/9848852.html

一、前言

    最近在看《架構整潔之道》一書,書中反覆提到了面向物件編程的 SOLID 原則(在作者的前一本書《代碼整潔之道》也是被大力闡釋),而面向切麵編程(Aop)作為面向物件編程的有力補充,對實踐整潔代碼更是如虎添翼。

    除了整潔之道系列的影響外,本文還致敬、借鑒、補充了Aspect Oriented Programming (AOP) in C# with SOLID一文。

    二、Aop 是什麼?


    在討論 Aop 之前,我們可以先看看一段很常見的代碼。

    public string GetSomeOne()

    {

        try

        {

           var result=DosomeThing();

            _logger.Information(result);

            return result;

        }

        catch (Exception e)

        {

            _logger.Error(e.Message);

            return null;

        }

    }

    public string GetOtherOne()

    {

        try

        {

            var result = DosomeThing();

            _logger.Information(result);

            return result;

        }

        catch (Exception e)

        {

            _logger.Error(e.Message);

            return null;

        }

    }

    這是一段很典型的面向過程的代碼,我們可以看到有相同的異常處理邏輯,如果我們想要避免重覆的代碼,我們至少可以把異常處理封裝一下:

    public string GetOtherOne()

    {

        return TryRun(()=> DosomeThing());

    }

    public T TryRun(Func action)

    {

        try

        {

           return action.Invoke();

        }

        catch (Exception e)

        {

            _logger.Error(e.Message);

            return default(T);

        }

    }

    代碼簡潔了很多,但是我們實際上是將真實的方法代碼與日誌代碼糾纏在一起,違反了 單一責任原則 。

    有沒有一種可能,讓我們不需要在原來的代碼上顯式呼叫 TryCache 呢?

    一個可能的答案是借助 AOP 來解決。使用 AOP,我們可以在不改變原來代碼的前提下,添加額外的單元功能(如異常處理,日誌處理、重試機制等)。 

    AOP 可以把原來一大串的面向過程的代碼重構成多個部分,聚焦於每一小部分,使我們的代碼 可讀性 和 維護性 更高,避免了 代碼重覆和代碼糾纏 的問題。

    三、裝飾器實現 AOP


    C# 可以使用的 Aop 框架有很多,在我們談論他們之前,我們可以先利用語言自帶的特性,實現基礎的 AOP 效果。 最簡單的形式莫過於 裝飾器樣式 ,它的雛形大致如下:

    public class TryHandler:IMyClient where TClient : IMyClient

    {

            private readonly TClient _client;

            private readonly ILogger _logger;

            public TryHandler(TClient client, ILogger logger)

            {

                _client = client;

                _logger = logger;

            }

            public string GetOtherOne()

            {

                try

                {

                    var result = DosomeThing();

                    return result;

                }

                catch (Exception e)

                {

                    _logger.Error(e.Message);

                    return null;

                }

            }

    }

    可以看到裝飾器只是在原來的物件上面擴展,符合 開放封閉原則。我們在呼叫的時候,只需顯式創建裝飾實體物件。

     var tryClient=new TryHandler(new MyClient());

     tryClient.GetOtherOne();

    細心的讀者可能還會發現,我們還可以在這個日誌裝飾器上面再附加一個裝飾器,比如一個針對結果處理的裝飾器。

    var resultClient=new ResultHandler>(tryClient);

    但是這樣的呼叫方法還是不盡人意,想象如果某個物件有三四個裝飾器,那麼我們創建實體的時候就需要多次傳遞。一個解決方法是 借助依賴註入 (DI) ,只需註冊一次服務型別,避免通過創建實體來獲取物件。

    另外,對於 .net core自帶的 DI 來說,更便捷的方法是借助開源類庫Scrutor 來註冊裝飾器物件。

    services.Decorate>();

    services.Decorate>();

    雖然解決了易用性,但是我們很快就發現了另一些不盡人意的地方,裝飾器樣式只能適用於 特定的型別,約束是比較強的。如果我們希望我們示例中的裝飾器可以實現通用,就需要找別的方法了。

    四、動態代理實現 Aop


    動態代理是指運行時生成,通過隱式重寫方法來附加額外的功能,而其中最流行的莫過於 Castle DynamicProxy了。

    Castle DynamicProxy 的常規用法是繼承 IInterceptor 接口,通過實現 Intercept 方法來處理代理的邏輯。

    public class DoSomethingAspect : IInterceptor

    {

        public void Intercept(IInvocation invocation)

        {

            try

            {

                DoSomething();

                invocation.Proceed();

            }

            catch (Exception ex)

            {

                throw;

            }

        }

        void DoSomething()

        {

        }

    }

    在呼叫的時候,類似裝飾器一樣需要創建代理實體。

    static void Main(string[] args)

    {

        var proxyClient = GetInterfaceProxy(new MyClient(),new DoSomethingAspect());

        proxyClient.GetOtherOne();

    }

    static T GetInterfaceProxy(T instance,params IInterceptor[] interceptors)

    {

        if (!typeof(T).IsInterface)

              throw new Exception(“T should be an interface”);

        ProxyGenerator proxyGenerator = new ProxyGenerator();

              return

                  (T)proxyGenerator.CreateInterfaceProxyWithTarget(typeof(T), instance, interceptors);

    }

    有很多開源專案在使用 Castle DynamicProxy,其穩定性和可靠性是值得信賴的,更多的使用方法可以參照官方示例或者第三方開源專案的代碼。需要特別註意的是,Castle DynamicProxy 只能作用於接口或者虛方法,這是動態代理的特性(局限)。

    除了 Castle DynamicProxy 外, AspectCore也是一個不錯的選擇。AspectCore 的快速簡單應用通過繼承 AbstractInterceptorAttribute 的 Attribute類來標記並攔截代理對應的接口或者虛方法(更詳細的用法可以參考 作者寫的使用方法)。

    public interface ICustomService

    {

        [CustomInterceptor]

        void Call();

    }

    public class CustomInterceptorAttribute : AbstractInterceptorAttribute 

    {

        public async override Task Invoke(AspectContext context, AspectDelegate next)

        {

            try

            {

                Console.WriteLine(“Before service call”);

                await next(context);

            }

            catch (Exception)

            {

                Console.WriteLine(“Service threw an exception!”);

                throw;

            }

            finally

            {

                Console.WriteLine(“After service call”);

            }

         }

     }

    雖然易用性很好,但是要註意使用的場合,如果是在低層次(如基礎設施層、應用入口層等)或者特定的應用模塊內使用,對整體架構影響不大。

    如果是在高層次(邏輯層、核心層、領域層等)使用,則會帶來不必要的依賴污染。

    所以並不是推薦使用這種 Attribute 攔截代理的方式,好在 AspectCore 的設計考慮到解耦的需要,可以在單獨配置代理攔截。

    serviceCollection.ConfigureDynamicProxy(config =>

    {

     config.Interceptors.AddTyped(Predicates.ForMethod(“ICustomService”, “Call”));

    });

    但是不管是 Castle DynamicProxy 還是 AspectCore 都只能作用與接口或者虛方法,這也是動態代理的局限(特性)。

    如果我們想要在不受限制地在非虛方法上實現 AOP 的效果,就需要別的方法了。

    五、編譯時織入實現 AOP


    進行 AOP 的另一種方法是通過編譯時織入,在編譯的程式集內部的方法中添加額外的 IL 代碼,附加我們想要的功能。

    PostSharp 是其中比較流行的一種,然而由於其商業化的性質,在這裡不做過多介紹。開源方面,Fody 是其中的佼佼者。

    Fody 在編譯時使用 Mono.Cecil 修改 . net 程式集的 IL 代碼。如果你沒有 IL 代碼方面的知識,可以直接使用基於 Fody 開發的插件。其中最流行的插件是Costura和 Virtuosity。Costura 將依賴項作為資源嵌入,實現多個 DLL 檔案合併成一個 exe https://www.cnblogs.com/instance/p/4863811.html的功能,而 Virtuosity 則是在構建的時候將所有成員更改為 virtual ,重寫 ORM (如EF的導航屬性、NHibernate)、 Mock(RhinoMocks、NMock)以及前面提到的動態代理中需要 virtual 的地方為 virtual。

    Fody 中的插件還有很多,除了 Costura 和 Virtuosity 之外,我個人還使用過 MethodDecorator,實現編譯時重寫類的方法或者建構式來實現 AOP 的效果。

    所有 Fody 的插件,首先都必須引入一個 FodyWeavers.xml ,並宣告使用的插件。

     

    不同的插件在後面的使用方法會有所不同,以 MethodDecorator 為例,我們需要新建一個特定格式的 Attribute 類,然後標記在特定的類方法上面。

    public class TestService 

    {

        [FodyTestAttribute]

        public void DoSomething()

        {

        }

    }

    [AttributeUsage(AttributeTargets.Method  | AttributeTargets.Module)]

    public class FodyTestAttribute : Attribute

    {

        protected object InitInstance;

        protected MethodBase InitMethod;

        protected Object[] Args;

        public void Init(object instance, MethodBase method, object[] args)

        {

            InitMethod = method;

            InitInstance = instance;

            Args = args;

        }

        public void OnEntry()

        {

            Console.WriteLine(“Before”);

        }

        public void OnExit()

        {

           Console.WriteLine(“After”);

        }

        public void OnException(Exception exception)

        {

        }

    }

    最後還需要一個 AssemblyInfo.cs 來配置哪些 Attribute 類產生作用。

    //AssemblyInfo.cs

    using System;

    [module: FodyTest]

    重新編譯生成,在輸出中還可以看到 Fody 的輸出。

    既然我們可以在編譯時織入 IL 代碼,那麼我們是不是可以提前生成我們想要的 AOP 效果,比如說借助代碼生成器。

    六、代碼生成器實現 AOP 效果

    T4是常見的文本生成框架,我們可以使用此工具在設計時生成代碼。前面我們提到過裝飾器樣式有特異性的問題,只能針對特定型別實現 AOP 效果,而借助代碼生成器,我們可以直接生成對應的代碼模板,避免了重覆的勞動。由於我個人對 T4 沒什麼使用經驗,有興趣的讀者可以參考Aspect Oriented Programming (AOP) in C# via T4 一文。

    除了 T4 之外,Roslyn 也是一個強有力的工具,已經有人基於 Roslyn 實現 AOP 的效果,將 Roslyn 封裝為 dotnet 全域性工具 ,針對特定的檔案插入指定的代碼段,有興趣的讀者可以參考 AOP_With_Roslyn 的代碼示例。

    結語


    AOP 是我們 避免代碼重覆 和 增強代碼可讀性 的有力工具,是我們編寫整潔代碼的有力保證,借助 C# 語言自身的特性和諸多強大的開源工具,使我們更專註於代碼功能。



    ●編號170,輸入編號直達本文

    ●輸入m獲取文章目錄

    推薦↓↓↓

    Web開發

    更多推薦18個技術類公眾微信

    涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等

赞(0)

分享創造快樂