最近在看《架構整潔之道》一書,書中反覆提到了面向對象編程的 SOLID 原則(在做者的前一本書《代碼整潔之道》也是被大力闡釋),而面向切面編程(Aop)做爲面向對象編程的有力補充,對實踐整潔代碼更是如虎添翼。html
除了整潔之道系列的影響外,本文還致敬、借鑑、補充了Aspect Oriented Programming (AOP) in C# with SOLID一文。git
在討論 Aop 以前,咱們能夠先看看一段很常見的代碼。github
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<String>(()=> DosomeThing()); } public T TryRun<T>(Func<T> action) { try { return action.Invoke(); } catch (Exception e) { _logger.Error(e.Message); return default(T); } }
代碼簡潔了不少,可是咱們其實是將真實的方法代碼與日誌代碼糾纏在一塊兒,違反了 單一責任原則 。有沒有一種可能,讓咱們不須要在原來的代碼上顯式調用 TryCache 呢?架構
一個可能的答案是藉助 AOP 來解決。使用 AOP,咱們能夠在不改變原來代碼的前提下,添加額外的單元功能(如異常處理,日誌處理、重試機制等)。 AOP 能夠把原來一大串的面向過程的代碼重構成多個部分,聚焦於每一小部分,使咱們的代碼 可讀性 和 維護性 更高,避免了 代碼重複和代碼糾纏 的問題。框架
C# 可使用的 Aop 框架有不少,在咱們談論他們以前,咱們能夠先利用語言自帶的特性,實現基礎的 AOP 效果。 最簡單的形式莫過於 裝飾器模式 ,它的雛形大體以下:async
public class TryHandler<TClient>: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; } } }
能夠看到裝飾器只是在原來的對象上面擴展,符合 開放封閉原則。咱們在調用的時候,只需顯式建立裝飾實例對象。ide
var tryClient=new TryHandler<MyClient>(new MyClient()); tryClient.GetOtherOne();
細心的讀者可能還會發現,咱們還能夠在這個日誌裝飾器上面再附加一個裝飾器,好比一個針對結果處理的裝飾器。函數
var resultClient=new ResultHandler<TryHandler<MyClient>>(tryClient);
可是這樣的調用方法仍是不盡人意,想象若是某個對象有三四個裝飾器,那麼咱們建立實例的時候就須要屢次傳遞。一個解決方法是 藉助依賴注入 (DI) ,只需註冊一次服務類型,避免經過建立實例來獲取對象。另外,對於 .net core自帶的 DI 來講,更便捷的方法是藉助開源類庫Scrutor 來註冊裝飾器對象。工具
services.Decorate<IMyClient, TryHandler<MyClient>>(); services.Decorate<IMyClient, ResultHandler<MyClient>>();
雖然解決了易用性,可是咱們很快就發現了另外一些不盡人意的地方,裝飾器模式只能適用於 特定的類型,約束是比較強的。若是咱們但願咱們示例中的裝飾器能夠實現通用,就須要找別的方法了。
動態代理是指運行時生成,經過隱式重寫方法來附加額外的功能,而其中最流行的莫過於 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<IMyClient>(new MyClient(),new DoSomethingAspect()); proxyClient.GetOtherOne(); } static T GetInterfaceProxy<T>(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<CustomInterceptorAttribute>(Predicates.ForMethod("ICustomService", "Call")); });
可是不論是 Castle DynamicProxy 仍是 AspectCore 都只能做用與接口或者虛方法,這也是動態代理的侷限(特性)。若是咱們想要在不受限制地在非虛方法上實現 AOP 的效果,就須要別的方法了。
進行 AOP 的另外一種方法是經過編譯時織入,在編譯的程序集內部的方法中添加額外的 IL 代碼,附加咱們想要的功能。
PostSharp 是其中比較流行的一種,然而因爲其商業化的性質,在這裏不作過多介紹。開源方面,Fody 是其中的佼佼者。
Fody 在編譯時使用 Mono.Cecil 修改 . net 程序集的 IL 代碼。若是你沒有 IL 代碼方面的知識,能夠直接使用基於 Fody 開發的插件。其中最流行的插件是Costura和 Virtuosity。Costura 將依賴項做爲資源嵌入,實現多個 DLL 文件合併成一個 exe 的功能,而 Virtuosity 則是在構建的時候將全部成員更改成 virtual
,重寫 ORM (如EF的導航屬性、NHibernate)、 Mock(RhinoMocks、NMock)以及前面提到的動態代理中須要 virtual
的地方爲 virtual
。
Fody 中的插件還有不少,除了 Costura 和 Virtuosity 以外,我我的還使用過 MethodDecorator,實現編譯時重寫類的方法或者構造函數來實現 AOP 的效果。
全部 Fody 的插件,首先都必須引入一個 FodyWeavers.xml
,並聲明使用的插件。
<?xml version="1.0" encoding="utf-8"?> <!--FodyWeavers.xml--> <Weavers> <MethodDecorator /> </Weavers>
不一樣的插件在後面的使用方法會有所不一樣,以 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 效果,好比說藉助代碼生成器。
T4是常見的文本生成框架,咱們可使用此工具在設計時生成代碼。前面咱們提到過裝飾器模式有特異性的問題,只能針對特定類型實現 AOP 效果,而藉助代碼生成器,咱們能夠直接生成對應的代碼模板,避免了重複的勞動。因爲我我的對 T4 沒什麼使用經驗,有興趣的讀者能夠參考Aspect Oriented Programming (AOP) in C# via T4 一文。
除了 T4 以外,Roslyn 也是一個強有力的工具,已經有人基於 Roslyn 實現 AOP 的效果,將 Roslyn 封裝爲 dotnet 全局工具 ,針對特定的文件插入指定的代碼段,有興趣的讀者能夠參考 AOP_With_Roslyn 的代碼示例。
AOP 是咱們 避免代碼重複 和 加強代碼可讀性 的有力工具,是咱們編寫整潔代碼的有力保證,藉助 C# 語言自身的特性和諸多強大的開源工具,使咱們更專一於代碼功能。