C# 中使用面向切面程式設計(AOP)中實踐程式碼整潔
1. 前言
最近在看《架構整潔之道》一書,書中反覆提到了面向物件程式設計的 SOLID 原則(在作者的前一本書《程式碼整潔之道》也是被大力闡釋),而面向切面程式設計(Aop)作為面向物件程式設計的有力補充,對實踐整潔程式碼更是如虎添翼。
1. 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<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 可以把原來一大串的面向過程的程式碼重構成多個部分,聚焦於每一小部分,使我們的程式碼 可讀性 和 維護性 更高,避免了 程式碼重複和程式碼糾纏 的問題。
2. 裝飾器實現 AOP
C# 可以使用的 Aop 框架有很多,在我們談論他們之前,我們可以先利用語言自帶的特性,實現基礎的 AOP 效果。 最簡單的形式莫過於 裝飾器模式
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;
}
}
}
可以看到裝飾器只是在原來的物件上面擴充套件,符合 開放封閉原則。我們在呼叫的時候,只需顯式建立裝飾例項物件。
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>>();
雖然解決了易用性,但是我們很快就發現了另一些不盡人意的地方,裝飾器模式只能適用於 特定的型別,約束是比較強的。如果我們希望我們示例中的裝飾器可以實現通用,就需要找別的方法了。
3. 動態代理實現 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<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 的效果,就需要別的方法了。
4. 編譯時織入實現 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 效果,比如說藉助程式碼生成器。
5. 程式碼生成器實現 AOP 效果
T4是常見的文字生成框架,我們可以使用此工具在設計時生成程式碼。前面我們提到過裝飾器模式有特異性的問題,只能針對特定型別實現 AOP 效果,而藉助程式碼生成器,我們可以直接生成對應的程式碼模板,避免了重複的勞動。由於我個人對 T4 沒什麼使用經驗,有興趣的讀者可以參考Aspect Oriented Programming (AOP) in C# via T4 一文。
除了 T4 之外,Roslyn 也是一個強有力的工具,已經有人基於 Roslyn 實現 AOP 的效果,將 Roslyn 封裝為 dotnet 全域性工具 ,針對特定的檔案插入指定的程式碼段,有興趣的讀者可以參考 AOP_With_Roslyn 的程式碼示例。
結語
AOP 是我們 避免程式碼重複 和 增強程式碼可讀性 的有力工具,是我們編寫整潔程式碼的有力保證,藉助 C# 語言自身的特性和諸多強大的開源工具,使我們更專注於程式碼功能。