本篇目錄
本系列的源碼本人已托管於Coding上:點擊查看,想要注冊Coding的可以點擊該連接注冊。
本系列的實驗環境:VS 2013 Update 5(建議最好使用集成了Nuget的VS版本,VS Express版也夠用)。
這篇博客覆蓋的內容包括:
- AOP簡史
- AOP解決什么問題
- 使用PostSharp編寫一個簡單的切面
AOP是什么?
AOP在計算機科學領域還是相對年輕的概念,由Xerox PARC公司發明。Gregor Kiczales 在1997年領導一隊研究人員首次介紹了AOP。當時他們關心的問題是如何在大型面向對象的代碼庫中重復使用那些必要且代價高的樣板,那些樣板的通用例子具有日志,緩存和事務功能。
在最終的“AOP”研究報告中,Kiczales和他的團隊描述了OOP技術不能捕獲和解決的問題,他們發現橫切關注點最終分散在整個代碼中,這種交錯的代碼會變得越來越難開發和維護。他們分析了所有技術原因,包括為何這種糾纏模式會出現,為什么避免起來這么困難,甚至涉及了設計模式的正確使用。
該報告描述了一種解決方案作為OOP的補充,即使用“切面aspects”封裝橫切關注點以及允許重復使用。最終實現了AspectJ,就是今天Java開發者仍然使用的一流AOP工具。
如果你想深入研究AOP的話,不妨讀一下該報告http://www.cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf。
該系列不會讓你覺得使用AOP很復雜,相反,只需要關注如何在.NET項目中使用AOP解決問題。
功能
AOP的目的:橫切關注點
推動AOP發明的主要驅動因素之一是OOP中橫切關注點的出現。橫切關注點是用於一個系統的多個部分的片段功能,它更偏向是一個架構概念而不是技術問題。橫切關注點和非功能需求有許多重疊:非功能需求經常橫切應用程序的多個部分。
功能需求和非功能需求
功能需求指項目中的增值需求,比如業務邏輯,UI,持久化(數據庫)。
非功能需求是項目中次要的,但卻不可缺少的元素,比如日志記錄,安全,性能和數據事務等等。
無論是否使用AOP,橫切關注點都是存在的。比如有個方法X,如果想要記錄日志C,那么該方法必須執行X和C。如果需要為方法Y和Z記錄日志,那么必須在每個方法中放置C。這里,C就是橫切關注點。
切面的任務:通知(Advice)
通知就是執行橫切關注點的代碼,比如對於橫切關注點logging,該代碼可能是log4net或者NLog的庫的調用,也可能是單條語句如Log.Write ("information")
或檢查和記錄參數,時間戳,性能指標等的批量邏輯。
Advice相當於AOP的“what”,下面看看“where”。
切面的映射:切入點(PointCut)
PointCut相當於AOP的“where”,在定義一個切入點之前,先要定義一個連接點(join point)。連接點就是程序執行的邏輯步驟之間的地方。為了方便理解,看一下下面的代碼:
nameService.SaveName();//nameService 是 NameService類型
nameService.GetListOfNames();
addressService.SaveAddress();//addressService 是 AddressService類型
以上代碼中的任何一個間隙都可以看作是一個連接點。只說這一句話,你肯定還是不知道有多少連接點。我們用圖示的方式來解釋一下,就解釋第一行代碼:
看見了吧?只第一行代碼就3個連接點,現在你應該明白連接點的意思了吧!現在再來看看切入點,一個切入點是一系列連接點(或者一個描述一系列連接點的表達式)。舉個例子,一個連接點是“調用svc.SaveName()之前”,那么一個切入點就是“調用任何方法之前”。切入點可以很簡單,比如“類中的每個方法之前”,也可以很復雜,比如“MyServices命名空間下的類的每個方法,除了私有方法和DeleteName方法”。
假設我想在NameService對象的退出連接點插入advice(一些代碼段),切入點就可以表達為“NameService的方法退出時”。如何在代碼中表達依賴於你正在使用的AOP工具的切入點呢?事實上,可以定義一個連接點不意味着使用工具可以到達該連接。一些連接點太低級了,一般不可行。
一旦確認了advice(what)和pointcut(where),就可以定義切面了。切面通過叫做編織(weaving)的過程工作。
AOP如何工作:編織(Weaving)
沒有AOP的時候,橫切關注點代碼經常是和核心業務邏輯混合在一個方法中的,這種方式就是傳說中的纏繞(tangling),因為核心業務邏輯和橫切關注點代碼就像意大利面條那樣纏繞在一起。當橫切關注點代碼用於多個方法和多個類時(一般使用復制,粘貼),這種方式叫做分散(scattering),因為代碼分散在整個應用中。用一張圖解釋如下:
使用AOP重構時,需要把所有的紅色代碼移到一個新類中,只保留執行業務邏輯的綠色代碼。然后通過指定一個切入點告訴AOP工具應用切面(紅色的類)到業務類(綠色的類)上。AOP工具執行這個連接步驟的過程就叫編織(weaving),如下圖:
優勢
使用AOP的主要優勢是精簡代碼,從而使得容易閱讀,更不容易出bug,以及容易維護。
使得代碼容易閱讀很重要,因為這樣會使得團隊成員很舒服並加速閱讀。而且,未來你也會感謝你。因為你或許被一個月前寫的代碼搞暈過。AOP允許你將纏繞的代碼移到它自己的類中,從而使得代碼更清晰,更具有陳述性。
AOP可以降低維護開銷,當然,使得代碼更容易閱讀就會使得維護更容易,此外,如果你在項目中使用了處理線程的樣板代碼片段,並且重用了,那么必須到處修復或更改代碼。如果使用AOP重構代碼到封裝的切面中,只需要在一個地方更改代碼就可以了。
清除意大利面條式代碼
你可能聽過“溫水煮青蛙”的故事,如果要求你在一個大型代碼庫中添加很多橫切關注點,你可能拒絕每次都在一個方法中添加那些代碼。但是如果在一個新的項目中或給一個小項目添加功能時,可能只需要幾行代碼,並且也重復不了幾次,你可能就會想着先復制、粘貼,以后再重構精簡一下。
“只要能跑起來”的誘惑是很強的,所以才會復制、粘貼,這種分散的或者纏繞的代碼已經被分類為反模式(antipattern),叫做散彈式修改。為什么叫散彈式修改?因為除了主要的業務邏輯,經過反復的復制、粘貼,代碼和其他的代碼混合在一起,更像散彈殼爆炸向整個目標擴散,所以形象地成為“散彈式修改”。單一職責原則(Single Responsibility Principle)就是為了避免這種模式的:一個類應該只有一個要修改的原因。
反模式(Antipatterns)
反模式是軟件工程已確認的一種模式,例如你可以在“Gang of Four book”(全名是:設計模式:可復用面向對象軟件的基礎)中找到任何模式,跟那些好的模式不同,反模式會導致bug,產生昂貴的維護費用以及令人頭疼的問題。
復制-粘貼策略可能會幫你快速解決問題,但長期看來,你最終的代碼會像昂貴的意大利苗條那樣糾纏不清,所以才有了有名的法則:Don't Repeat yourself(DRY)!
減少重復
你可能技術更牛一點或者不屑於使用復制-粘貼,你可能會使用比如DI或者裝飾者模式來處理橫切關注點。有進步,因為你這樣的話代碼就松耦合並且更容易測試。但談到橫切關注點時,當使用DI時,你最后可能仍然會讓代碼纏繞或分散。
試想,你已經將一個橫切關注點比如事務管理(begin/commit/rollback)重構到一個單獨的服務中,偽代碼可能像下面那樣:
public class InvoiceService {
ITransactionManagementService _transaction;
IInvoiceData _invoicedb;
InvoiceService(IInvoiceData invoicedb,
ITransactionManagementService transaction)//實例化類時,必須傳入兩個服務,其中一個是處理橫切關注點的
{
_invoicedb = invoicedb;
_transaction = transaction;
}
void CreateInvoice(ShoppingCart cart) {//CreateInvoice方法必須管理事務的開始和結束,以及核心的業務邏輯
_transaction.Start();//即使使用了依賴注入,依賴的使用仍是纏繞的
_invoicedb.CreateNewInvoice();
foreach(item in cart)
_invoicedb.AddItem(item);
_invoicedb.ProcessSalesTax();
_transaction.Commit();
}
}
正如代碼中解釋的那樣,雖然使用DI比將事務代碼硬編碼到每個方法更好,而且事務管理是松耦合的,但是InvoiceService
中的代碼仍然是纏繞的:因為 _transaction.Start()和 _transaction.Commit()
仍然存在該服務中。這種方法會使得單元測試更加棘手,因為依賴越多,需要使用的偽造(stubs/fakes)越多。
如果熟悉DI,相信你也應該熟悉裝飾者模式。假設InvoiceService
類有個接口IInvoiceService
,那么我們就可以定義一個裝飾者來處理所有的事務,它也實現了IInvoiceService
,這樣就可以通過構造函數傳入一個真實的InvoiceService
依賴了,代碼如下:
public class TransactionDecorator : IInvoiceData //裝飾者實現了相同的接口
{
IInvoiceData _realService;
ITransactionManagementService _transaction;
public TransactionDecorator( IInvoiceData svc,//依賴於正在裝飾的服務
ITransactionManagementService _trans )//依賴於事務實現
{
_realService = svc;
_transaction = trans;
}
public void CreateInvoice( ShoppingCart cart )
{
_transaction.Start();//事務現在位於裝飾者中
_realService.CreateInvoice( cart );//調用裝飾的方法
_transaction.End();
}
}
該裝飾者以及所有的依賴都是使用IoC工具(比如,StructureMap)配置的,而不是直接使用InvoiceService
。現在,我們遵守開閉原則,擴展InvoiceService
,不用修改InvoiceService
類就可以添加事務管理,這是個好的開始,有時這種方法對於小項目處理橫切關注點足夠了。
但是思考一下這種方法的缺點,尤其是隨着項目的成長,諸如logging或事物管理的橫切關注點可能會應用在不同的類中,有了這個裝飾者,只能讓InvoiceService
這一個類簡潔一些,如果有其他的類,就需要為其他的類寫裝飾者。如果有1000個這樣的服務類呢,你要寫1000個裝飾者嗎?累死你!考慮一下這樣重復了多少!
某些時候,如果要定義3到100個裝飾者(多少取決於你),那么就可以拋棄裝飾者而轉向使用一個切面了。切面跟裝飾者很相似,但是使用AOP工具會使得切面更具有通用目的。下面來寫一個切面類,然后使用特性指明切面應該使用的地方,如下:
public class InvoiceService
{
IInvoiceData _invoicedb;
InvoiceService( IInvoiceData invoicedb )//只傳入一個服務類
{
_invoicedb = invoicedb;
}
[TransactionAspect]
void CreateInvoice( ShoppingCart cart )//CreateInvoice方法不包含任何事務代碼
{
_invoicedb.CreateNewInvoice();
foreach ( item in cart )
_invoicedb.AddItem( item );
}
}
public class TransactionAspect {
ITransactionManagementService _transaction;
TransactionAspect( ITransactionManagementService transaction )
{
_transaction = transaction;
}
void OnEntry()
{
_transaction.Start();//事務Start移到了切面的OnEntry方法中
}
void OnSuccess()
{
_transaction.Commit();
}
}
注意,AOP絕不能完全取代DI(也不應該取代)。InvoiceService
仍然使用了DI來獲取IInvoiceData
的實例,它對於執行業務邏輯是至關重要的,同時也不是橫切關注點。但ITransactionManagementService
不再是InvoiceService
的依賴了,它已經被移動到了切面中。這樣就沒有了任何纏繞的代碼,因為CreateInvoice
再也沒有了事務相關的代碼。
封裝
不需要1000個裝飾者,只需要一個切面足以,有了這個切面,就可以將橫切關注點封裝到一個類中。
下面是一個偽代碼類,由於橫切關注點而沒有遵守單一職責原則:
public class AddressBookService
{
public string GetPhoneNumber( string name )
{
if ( name is null )
throw new ArgumentException( "name" );
var entry = PhoneNumberDatabase.GetEntryByName( name );
return(entry.PhoneNumber);
}
}
雖然上面的代碼閱讀和維護都相當簡單,但是它做了兩件事:一是檢查傳入的name是否是有效的;二是基於傳入的name找到電話號碼。雖然檢查參數的有效性和服務方法相關,但是它仍然是可以分離和復用的輔助功能。下面是使用AOP重構之后的偽代碼:
public class AddressBookService
{
[CheckForNullArgumentsAspect]
public string GetPhoneNumber( string name )
{
var entry = PhoneNumberDatabase.GetEntryByName( name );
return(entry.PhoneNumber);
}
}
public class CheckForNullArgumentsAspect
{
public void OnEntry( MethodInformation method )
{
foreach ( arg in method.Arguments )
if ( arg is null )
throw ArgumentException( arg.name )
}
}
這個例子中的OnEntry
方法多了個MethodInformation
參數,它提供了一些關於方法的信息,為的是可以檢測方法的參數是否為null。雖然這個方法微不足道,但是CheckForNullArgumentsAspect
代碼可以復用到確保參數有效的其他方法上。
public class AddressBookService
{
[CheckForNullArgumentAspect]
public string GetPhoneNumber( string name )
{
...
}
}
public class InvoiceService
{
[CheckForNullArgumentAspect]
public Invoice GetInvoiceByName( string name )
{
...
}
[CheckForNullArgumentAspect]
public void CreateInvoice( ShoppingCart cart )
{
...
}
}
public class PaymentSevice
{
[CheckForNullArgumentAspect]
public Payment FindPaymentByInvoice( string invoiceId )
{
...
}
}
這樣一來,如果我們想要修改和Invoice相關的東西,只需要修改InvoiceService
。如果想要修改和null檢測相關的一些事情,只需要修改CheckForNullArgumentAspect
。涉及到的每個類只有一個原因修改。現在我們就不太可能因為修改造成bug或倒退。
AOP就在你的日常開發中
作為一名.NET 開發人,你可能每天都在做着很多普通的事情,這些事情就是AOP的一部分,例如:
- ASP.NET Forms認證
- ASP.NET的IHttpModule實現
- ASP.NET MVC認證
- ASP.NET MVC IActionFilter的實現
ASP.NET有一個可以實現和在web.config中安裝的IHttpModule。完成之后,對於web應用的每個頁面請求的每個模塊都會運行。在IHttpModule實現的內部,可以定義運行在請求開始時或請求結束時(分別是BeginRequest和EndRequest)的事件處理程序,然后,再創建一個邊界(boundary)切面:運行在頁面請求邊界的代碼。
如果使用了現成的forms認證,那么上面的這些已經默認實現了,ASP.NET Forms認證內部使用了Forms-AuthenticationModule
,它本身就是IHttpModule
的實現。不需要在每個頁面上使用代碼檢測認證,只需要巧妙地使用這個模塊封裝認證即可。如果認證更改了,只需要修改配置,而不是每個頁面。這樣,即使添加一個新頁面,也不會擔心忘記給它添加認證。
ASP.NET MVC應用程序也是一樣,我們也可以創建實現了IActionFilter
的Attribute
類。這些特性可以應用於action方法,它們會在action方法執行前后運行(分別是OnActionExecuting和OnActionExecuted)。如果在一個新的ASP.NET MVC項目中,使用了默認的AccountController
,那么你很可能已經看到了action方法上的[Authorize]
特性。AuthorizeAtrribute
是IActionFilter
的內置實現,它會為我們處理forms認證而不需要在所有的控制器的action方法都添加認證代碼!
不僅僅是ASP.NET開發者,其他的開發者也一樣,他們可能已經看到並用到了AOP,但就是沒有意識到這是AOP。上面的例子都是在.NET框架中使用AOP的例子,如果你之前看到過類似的代碼,那么你應該清楚AOP如何幫助你了。
從下面開始,跟我動手敲代碼吧!你將會寫出第一個切面!
Hello,World!
現在我們正式開始寫第一個切面,在寫代碼時,我會指出AOP的一些特征(advice,pointcut等等),不要擔心你是否能完全理解正在做什么,只需要跟着我做即可。
下面創建一個控制台應用程序,取名AopFirstDemo:
然后,打開VS的程序包管理器控制台,輸入Install-Package postsharp
安裝PostSharp(當然,也可以通過可視化的方式安裝,這里不解釋了)。
這里雖然安裝了postsharp的程序包,但是你還得安裝PostSharp的擴展,安裝了擴展之后會有一個45天的有效期(因為PostSharp是收費的),此外,PostSharp 的Express版是商用免費的,因此,我們也可以在工作中使用這個免費版的(仍然需要許可,但是是一個免費許可)。安裝了postsharp之后,就可以在解決方案資源管理器的引用中看到項目中添加了PostSharp引用。
現在定義一個簡單的類和方法如下:
class MyClass
{
public void MyMehtod()
{
Console.WriteLine("Hello,AOP!");
}
}
在Main方法中實例化MyClass
,並調用該方法,代碼如下:
static void Main(string[] args)
{
var obj = new MyClass();
obj.MyMehtod();
Console.Read();
}
以上代碼很簡單,相信初學C#的人都會知道什么意思,就不解釋了!
繼續深入關於切面,在創建一個切面之前,我們先要明確一點:這個切面要處理什么橫切關注點。這里為了簡單,我們定義的需求很簡單,在方法執行前后分別輸出"方法執行前"和"方法執行后"。因為這個切面可以被其他的類復用,所以我們必須創建一個新類MyAspect,它繼承自OnMehodBoundaryAspect
(它是PostSharp.Aspects命名空間的一個基類),代碼如下:
[Serializable]
public class MyAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Console.WriteLine("方法執行前");
}
public override void OnExit(MethodExecutionArgs args)
{
Console.WriteLine("方法執行后");
}
}
PostSharp要求切面類必須是Serializable
(因為PostSharp在編譯時實例化切面,這樣它們就可以在編譯時和運行時持久存在,后面的系列還會說的,看官莫急)。
還記得連接點嗎?每個方法都有邊界連接點:方法開始之前,結束之后,拋出異常時,正常結束時(在PostSharp中分別對應OnEntry,OnExit,OnException和OnSuccess
)。
注意一下 MethodExecutionArgs
參數,它提供了關於綁定方法的信息和上下文。這個簡單的例子中沒用它,但是在真實項目中這個參數會經常使用。
這個切面的Advice(通知)只是簡單地輸出了一句話。現在,切面定義好了,但是在哪個方法前后輸出信息呢?最基本的方式就是告訴PostSharp該切面以特性的方式用在哪個方法上。比如,將MyAspect
切面以特性的形式用在之前創建的“Hello,AOP!”的MyMethod
方法上:
class MyClass
{
[MyAspect]
public void MyMehtod()
{
Console.WriteLine("Hello,AOP!");
}
}
現在,再次運行程序。在程序編譯完成之后,PostSharp會接管並執行Weaving(編織)。因為PostSharp是一個post compilerAOP 工具,因此它會在程序編譯之后、執行之前修改程序。
執行結果如下:
特性(Attributes)
事實上,使用PostSharp時沒必要在每個代碼段上都添加特性,請繼續關注該博客,后面會講PostSharp的多播特性。在介紹多播特性之前,我們為了簡單先使用單個特性。
現在,我們已經寫了一個切面,並告訴PostSharp在那里使用它,以及PostSharp已經執行了編織。這個簡單的例子也許吸引不了你,但是注意你沒有對MyMethod
本身做任何修改,就可以把代碼放到它的周圍,當然,要使用[MyAspect]特性才行。此外,使用特性並不是使用AOP的唯一方式:例如Castle DynamicProxy使用了IoC工具,這個后面再講。
小結
AOP並沒有聽上去那么復雜,你可能需要花費點時間來習慣,因為你可能必須要調整思考橫切關注點的方式。
AOP是一個鼓舞人心的、強大的工具,並且使用起來很有趣。本系列教程將使用的AOP工具是PostSharp和Castle DynamicProxy,如果你不喜歡,你可以選擇其他的AOP工具,見下表:
編譯時AOP工具
- PostSharp
- LinFu
- SheepAspect
- Fody
- CIL操作工具
運行時AOP工具
- Castle Windsor/DynamicProxy
- StructureMap
- Unity
- Spring.NET
最后,無論你選擇的是什么工具,AOP都會更加有效地完成工作:再也不用復制-粘貼相同的樣板代碼了或者在樣板代碼中修復相同的bug達到上百次。在抽象層面上,這會幫你有效地堅持單一職責原則和 開閉原則。在真實項目中,你會將更多的時間花在增值的功能上而不是那些乏味的工作上。總之,掌握了AOP,會讓你事半功倍,愛上Code!