這篇博客覆蓋的內(nèi)容包括:
- AOP簡史
- AOP解決什么問題
- 使用PostSharp編寫一個簡單的切面
AOP是什么?
AOP在計算機科學(xué)領(lǐng)域還是相對年輕的概念,由Xerox PARC公司發(fā)明。Gregor Kiczales 在1997年領(lǐng)導(dǎo)一隊研究人員首次介紹了AOP。當時他們關(guān)心的問題是如何在大型面向?qū)ο蟮拇a庫中重復(fù)使用那些必要且代價高的樣板,那些樣板的通用例子具有日志,緩存和事務(wù)功能。
在最終的“AOP”研究報告中,Kiczales和他的團隊描述了OOP技術(shù)不能捕獲和解決的問題,他們發(fā)現(xiàn)橫切關(guān)注點最終分散在整個代碼中,這種交錯的代碼會變得越來越難開發(fā)和維護。他們分析了所有技術(shù)原因,包括為何這種糾纏模式會出現(xiàn),為什么避免起來這么困難,甚至涉及了設(shè)計模式的正確使用。
該報告描述了一種解決方案作為OOP的補充,即使用“切面aspects”封裝橫切關(guān)注點以及允許重復(fù)使用。最終實現(xiàn)了AspectJ,就是今天Java開發(fā)者仍然使用的一流AOP工具。
如果你想深入研究AOP的話,不妨讀一下該報告http://www.cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf。
該系列不會讓你覺得使用AOP很復(fù)雜,相反,只需要關(guān)注如何在.NET項目中使用AOP解決問題。
功能
AOP的目的:橫切關(guān)注點
推動AOP發(fā)明的主要驅(qū)動因素之一是OOP中橫切關(guān)注點的出現(xiàn)。橫切關(guān)注點是用于一個系統(tǒng)的多個部分的片段功能,它更偏向是一個架構(gòu)概念而不是技術(shù)問題。橫切關(guān)注點和非功能需求有許多重疊:非功能需求經(jīng)常橫切應(yīng)用程序的多個部分。
功能需求和非功能需求
功能需求指項目中的增值需求,比如業(yè)務(wù)邏輯,UI,持久化(數(shù)據(jù)庫)。
非功能需求是項目中次要的,但卻不可缺少的元素,比如日志記錄,安全,性能和數(shù)據(jù)事務(wù)等等。
無論是否使用AOP,橫切關(guān)注點都是存在的。比如有個方法X,如果想要記錄日志C,那么該方法必須執(zhí)行X和C。如果需要為方法Y和Z記錄日志,那么必須在每個方法中放置C。這里,C就是橫切關(guān)注點。
切面的任務(wù):通知(Advice)
通知就是執(zhí)行橫切關(guān)注點的代碼,比如對于橫切關(guān)注點logging,該代碼可能是log4net或者NLog的庫的調(diào)用,也可能是單條語句如Log.Write ("information")或檢查和記錄參數(shù),時間戳,性能指標等的批量邏輯。
Advice相當于AOP的“what”,下面看看“where”。
切面的映射:切入點(PointCut)
PointCut相當于AOP的“where”,在定義一個切入點之前,先要定義一個連接點(join point)。連接點就是程序執(zhí)行的邏輯步驟之間的地方。為了方便理解,看一下下面的代碼:
nameService.SaveName();//nameService 是 NameService類型
nameService.GetListOfNames();
addressService.SaveAddress();//addressService 是 AddressService類型
以上代碼中的任何一個間隙都可以看作是一個連接點。只說這一句話,你肯定還是不知道有多少連接點。我們用圖示的方式來解釋一下,就解釋第一行代碼:

看見了吧?只第一行代碼就3個連接點,現(xiàn)在你應(yīng)該明白連接點的意思了吧!現(xiàn)在再來看看切入點,一個切入點是一系列連接點(或者一個描述一系列連接點的表達式)。舉個例子,一個連接點是“調(diào)用svc.SaveName()之前”,那么一個切入點就是“調(diào)用任何方法之前”。切入點可以很簡單,比如“類中的每個方法之前”,也可以很復(fù)雜,比如“MyServices命名空間下的類的每個方法,除了私有方法和DeleteName方法”。
假設(shè)我想在NameService對象的退出連接點插入advice(一些代碼段),切入點就可以表達為“NameService的方法退出時”。如何在代碼中表達依賴于你正在使用的AOP工具的切入點呢?事實上,可以定義一個連接點不意味著使用工具可以到達該連接。一些連接點太低級了,一般不可行。
一旦確認了advice(what)和pointcut(where),就可以定義切面了。切面通過叫做編織(weaving)的過程工作。
AOP如何工作:編織(Weaving)
沒有AOP的時候,橫切關(guān)注點代碼經(jīng)常是和核心業(yè)務(wù)邏輯混合在一個方法中的,這種方式就是傳說中的纏繞(tangling),因為核心業(yè)務(wù)邏輯和橫切關(guān)注點代碼就像意大利面條那樣纏繞在一起。當橫切關(guān)注點代碼用于多個方法和多個類時(一般使用復(fù)制,粘貼),這種方式叫做分散(scattering),因為代碼分散在整個應(yīng)用中。用一張圖解釋如下:

使用AOP重構(gòu)時,需要把所有的紅色代碼移到一個新類中,只保留執(zhí)行業(yè)務(wù)邏輯的綠色代碼。然后通過指定一個切入點告訴AOP工具應(yīng)用切面(紅色的類)到業(yè)務(wù)類(綠色的類)上。AOP工具執(zhí)行這個連接步驟的過程就叫編織(weaving),如下圖:

優(yōu)勢
使用AOP的主要優(yōu)勢是精簡代碼,從而使得容易閱讀,更不容易出bug,以及容易維護。
使得代碼容易閱讀很重要,因為這樣會使得團隊成員很舒服并加速閱讀。而且,未來你也會感謝你。因為你或許被一個月前寫的代碼搞暈過。AOP允許你將纏繞的代碼移到它自己的類中,從而使得代碼更清晰,更具有陳述性。
AOP可以降低維護開銷,當然,使得代碼更容易閱讀就會使得維護更容易,此外,如果你在項目中使用了處理線程的樣板代碼片段,并且重用了,那么必須到處修復(fù)或更改代碼。如果使用AOP重構(gòu)代碼到封裝的切面中,只需要在一個地方更改代碼就可以了。
清除意大利面條式代碼
你可能聽過“溫水煮青蛙”的故事,如果要求你在一個大型代碼庫中添加很多橫切關(guān)注點,你可能拒絕每次都在一個方法中添加那些代碼。但是如果在一個新的項目中或給一個小項目添加功能時,可能只需要幾行代碼,并且也重復(fù)不了幾次,你可能就會想著先復(fù)制、粘貼,以后再重構(gòu)精簡一下。
“只要能跑起來”的誘惑是很強的,所以才會復(fù)制、粘貼,這種分散的或者纏繞的代碼已經(jīng)被分類為反模式(antipattern),叫做散彈式修改。為什么叫散彈式修改?因為除了主要的業(yè)務(wù)邏輯,經(jīng)過反復(fù)的復(fù)制、粘貼,代碼和其他的代碼混合在一起,更像散彈殼爆炸向整個目標擴散,所以形象地成為“散彈式修改”。單一職責(zé)原則(Single Responsibility Principle)就是為了避免這種模式的:一個類應(yīng)該只有一個要修改的原因。
反模式(Antipatterns)
反模式是軟件工程已確認的一種模式,例如你可以在“Gang of Four book”(全名是:設(shè)計模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ))中找到任何模式,跟那些好的模式不同,反模式會導(dǎo)致bug,產(chǎn)生昂貴的維護費用以及令人頭疼的問題。
復(fù)制-粘貼策略可能會幫你快速解決問題,但長期看來,你最終的代碼會像昂貴的意大利苗條那樣糾纏不清,所以才有了有名的法則:Don't Repeat yourself(DRY)!
減少重復(fù)
你可能技術(shù)更牛一點或者不屑于使用復(fù)制-粘貼,你可能會使用比如DI或者裝飾者模式來處理橫切關(guān)注點。有進步,因為你這樣的話代碼就松耦合并且更容易測試。但談到橫切關(guān)注點時,當使用DI時,你最后可能仍然會讓代碼纏繞或分散。
試想,你已經(jīng)將一個橫切關(guān)注點比如事務(wù)管理(begin/commit/rollback)重構(gòu)到一個單獨的服務(wù)中,偽代碼可能像下面那樣:
public class InvoiceService {
ITransactionManagementService _transaction;
IInvoiceData _invoicedb;
InvoiceService(IInvoiceData invoicedb,
ITransactionManagementService transaction)//實例化類時,必須傳入兩個服務(wù),其中一個是處理橫切關(guān)注點的
{
_invoicedb = invoicedb;
_transaction = transaction;
}
void CreateInvoice(ShoppingCart cart) {//CreateInvoice方法必須管理事務(wù)的開始和結(jié)束,以及核心的業(yè)務(wù)邏輯
_transaction.Start();//即使使用了依賴注入,依賴的使用仍是纏繞的
_invoicedb.CreateNewInvoice();
foreach(item in cart)
_invoicedb.AddItem(item);
_invoicedb.ProcessSalesTax();
_transaction.Commit();
}
}
正如代碼中解釋的那樣,雖然使用DI比將事務(wù)代碼硬編碼到每個方法更好,而且事務(wù)管理是松耦合的,但是InvoiceService中的代碼仍然是纏繞的:因為_transaction.Start()和 _transaction.Commit()仍然存在該服務(wù)中。這種方法會使得單元測試更加棘手,因為依賴越多,需要使用的偽造(stubs/fakes)越多。
如果熟悉DI,相信你也應(yīng)該熟悉裝飾者模式。假設(shè)InvoiceService類有個接口IInvoiceService,那么我們就可以定義一個裝飾者來處理所有的事務(wù),它也實現(xiàn)了IInvoiceService,這樣就可以通過構(gòu)造函數(shù)傳入一個真實的InvoiceService依賴了,代碼如下:
public class TransactionDecorator : IInvoiceData //裝飾者實現(xiàn)了相同的接口
{
IInvoiceData _realService;
ITransactionManagementService _transaction;
public TransactionDecorator( IInvoiceData svc,//依賴于正在裝飾的服務(wù)
ITransactionManagementService _trans )//依賴于事務(wù)實現(xiàn)
{
_realService = svc;
_transaction = trans;
}
public void CreateInvoice( ShoppingCart cart )
{
_transaction.Start();//事務(wù)現(xiàn)在位于裝飾者中
_realService.CreateInvoice( cart );//調(diào)用裝飾的方法
_transaction.End();
}
}
該裝飾者以及所有的依賴都是使用IoC工具(比如,StructureMap)配置的,而不是直接使用InvoiceService?,F(xiàn)在,我們遵守開閉原則,擴展InvoiceService,不用修改InvoiceService類就可以添加事務(wù)管理,這是個好的開始,有時這種方法對于小項目處理橫切關(guān)注點足夠了。
但是思考一下這種方法的缺點,尤其是隨著項目的成長,諸如logging或事物管理的橫切關(guān)注點可能會應(yīng)用在不同的類中,有了這個裝飾者,只能讓InvoiceService這一個類簡潔一些,如果有其他的類,就需要為其他的類寫裝飾者。如果有1000個這樣的服務(wù)類呢,你要寫1000個裝飾者嗎?累死你!考慮一下這樣重復(fù)了多少!
某些時候,如果要定義3到100個裝飾者(多少取決于你),那么就可以拋棄裝飾者而轉(zhuǎn)向使用一個切面了。切面跟裝飾者很相似,但是使用AOP工具會使得切面更具有通用目的。下面來寫一個切面類,然后使用特性指明切面應(yīng)該使用的地方,如下:
public class InvoiceService
{
IInvoiceData _invoicedb;
InvoiceService( IInvoiceData invoicedb )//只傳入一個服務(wù)類
{
_invoicedb = invoicedb;
}
[TransactionAspect]
void CreateInvoice( ShoppingCart cart )//CreateInvoice方法不包含任何事務(wù)代碼
{
_invoicedb.CreateNewInvoice();
foreach ( item in cart )
_invoicedb.AddItem( item );
}
}
public class TransactionAspect {
ITransactionManagementService _transaction;
TransactionAspect( ITransactionManagementService transaction )
{
_transaction = transaction;
}
void OnEntry()
{
_transaction.Start();//事務(wù)Start移到了切面的OnEntry方法中
}
void OnSuccess()
{
_transaction.Commit();
}
}
注意,AOP絕不能完全取代DI(也不應(yīng)該取代)。InvoiceService仍然使用了DI來獲取IInvoiceData的實例,它對于執(zhí)行業(yè)務(wù)邏輯是至關(guān)重要的,同時也不是橫切關(guān)注點。但ITransactionManagementService不再是InvoiceService的依賴了,它已經(jīng)被移動到了切面中。這樣就沒有了任何纏繞的代碼,因為CreateInvoice再也沒有了事務(wù)相關(guān)的代碼。
封裝
不需要1000個裝飾者,只需要一個切面足以,有了這個切面,就可以將橫切關(guān)注點封裝到一個類中。
下面是一個偽代碼類,由于橫切關(guān)注點而沒有遵守單一職責(zé)原則:
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找到電話號碼。雖然檢查參數(shù)的有效性和服務(wù)方法相關(guān),但是它仍然是可以分離和復(fù)用的輔助功能。下面是使用AOP重構(gòu)之后的偽代碼:
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參數(shù),它提供了一些關(guān)于方法的信息,為的是可以檢測方法的參數(shù)是否為null。雖然這個方法微不足道,但是CheckForNullArgumentsAspect代碼可以復(fù)用到確保參數(shù)有效的其他方法上。
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相關(guān)的東西,只需要修改InvoiceService。如果想要修改和null檢測相關(guān)的一些事情,只需要修改CheckForNullArgumentAspect。涉及到的每個類只有一個原因修改?,F(xiàn)在我們就不太可能因為修改造成bug或倒退。
AOP就在你的日常開發(fā)中
作為一名.NET 開發(fā)人,你可能每天都在做著很多普通的事情,這些事情就是AOP的一部分,例如:
- ASP.NET Forms認證
- ASP.NET的IHttpModule實現(xiàn)
- ASP.NET MVC認證
- ASP.NET MVC IActionFilter的實現(xiàn)
ASP.NET有一個可以實現(xiàn)和在web.config中安裝的IHttpModule。完成之后,對于web應(yīng)用的每個頁面請求的每個模塊都會運行。在IHttpModule實現(xiàn)的內(nèi)部,可以定義運行在請求開始時或請求結(jié)束時(分別是BeginRequest和EndRequest)的事件處理程序,然后,再創(chuàng)建一個邊界(boundary)切面:運行在頁面請求邊界的代碼。
如果使用了現(xiàn)成的forms認證,那么上面的這些已經(jīng)默認實現(xiàn)了,ASP.NET Forms認證內(nèi)部使用了Forms-AuthenticationModule,它本身就是IHttpModule的實現(xiàn)。不需要在每個頁面上使用代碼檢測認證,只需要巧妙地使用這個模塊封裝認證即可。如果認證更改了,只需要修改配置,而不是每個頁面。這樣,即使添加一個新頁面,也不會擔心忘記給它添加認證。

ASP.NET MVC應(yīng)用程序也是一樣,我們也可以創(chuàng)建實現(xiàn)了IActionFilter的Attribute類。這些特性可以應(yīng)用于action方法,它們會在action方法執(zhí)行前后運行(分別是OnActionExecuting和OnActionExecuted)。如果在一個新的ASP.NET MVC項目中,使用了默認的AccountController,那么你很可能已經(jīng)看到了action方法上的[Authorize]特性。AuthorizeAtrribute是IActionFilter的內(nèi)置實現(xiàn),它會為我們處理forms認證而不需要在所有的控制器的action方法都添加認證代碼!

不僅僅是ASP.NET開發(fā)者,其他的開發(fā)者也一樣,他們可能已經(jīng)看到并用到了AOP,但就是沒有意識到這是AOP。上面的例子都是在.NET框架中使用AOP的例子,如果你之前看到過類似的代碼,那么你應(yīng)該清楚AOP如何幫助你了。
從下面開始,跟我動手敲代碼吧!你將會寫出第一個切面!
Hello,World!
現(xiàn)在我們正式開始寫第一個切面,在寫代碼時,我會指出AOP的一些特征(advice,pointcut等等),不要擔心你是否能完全理解正在做什么,只需要跟著我做即可。
下面創(chuàng)建一個控制臺應(yīng)用程序,取名AopFirstDemo:

然后,打開VS的程序包管理器控制臺,輸入Install-Package postsharp安裝PostSharp(當然,也可以通過可視化的方式安裝,這里不解釋了)。
這里雖然安裝了postsharp的程序包,但是你還得安裝PostSharp的擴展,安裝了擴展之后會有一個45天的有效期(因為PostSharp是收費的),此外,PostSharp 的Express版是商用免費的,因此,我們也可以在工作中使用這個免費版的(仍然需要許可,但是是一個免費許可)。安裝了postsharp之后,就可以在解決方案資源管理器的引用中看到項目中添加了PostSharp引用。
現(xiàn)在定義一個簡單的類和方法如下:
class MyClass
{
public void MyMehtod()
{
Console.WriteLine("Hello,AOP!");
}
}
在Main方法中實例化MyClass,并調(diào)用該方法,代碼如下:
static void Main(string[] args)
{
var obj = new MyClass();
obj.MyMehtod();
Console.Read();
}
以上代碼很簡單,相信初學(xué)C#的人都會知道什么意思,就不解釋了!
繼續(xù)深入關(guān)于切面,在創(chuàng)建一個切面之前,我們先要明確一點:這個切面要處理什么橫切關(guān)注點。這里為了簡單,我們定義的需求很簡單,在方法執(zhí)行前后分別輸出"方法執(zhí)行前"和"方法執(zhí)行后"。因為這個切面可以被其他的類復(fù)用,所以我們必須創(chuàng)建一個新類MyAspect,它繼承自OnMehodBoundaryAspect(它是PostSharp.Aspects命名空間的一個基類),代碼如下:
[Serializable]
public class MyAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Console.WriteLine("方法執(zhí)行前");
}
public override void OnExit(MethodExecutionArgs args)
{
Console.WriteLine("方法執(zhí)行后");
}
}
PostSharp要求切面類必須是Serializable(因為PostSharp在編譯時實例化切面,這樣它們就可以在編譯時和運行時持久存在,后面的系列還會說的,看官莫急)。
還記得連接點嗎?每個方法都有邊界連接點:方法開始之前,結(jié)束之后,拋出異常時,正常結(jié)束時(在PostSharp中分別對應(yīng)OnEntry,OnExit,OnException和OnSuccess)。
注意一下 MethodExecutionArgs參數(shù),它提供了關(guān)于綁定方法的信息和上下文。這個簡單的例子中沒用它,但是在真實項目中這個參數(shù)會經(jīng)常使用。
這個切面的Advice(通知)只是簡單地輸出了一句話?,F(xiàn)在,切面定義好了,但是在哪個方法前后輸出信息呢?最基本的方式就是告訴PostSharp該切面以特性的方式用在哪個方法上。比如,將MyAspect切面以特性的形式用在之前創(chuàng)建的“Hello,AOP!”的MyMethod方法上:
class MyClass
{
[MyAspect]
public void MyMehtod()
{
Console.WriteLine("Hello,AOP!");
}
}
現(xiàn)在,再次運行程序。在程序編譯完成之后,PostSharp會接管并執(zhí)行Weaving(編織)。因為PostSharp是一個post compilerAOP 工具,因此它會在程序編譯之后、執(zhí)行之前修改程序。
執(zhí)行結(jié)果如下:

特性(Attributes)
事實上,使用PostSharp時沒必要在每個代碼段上都添加特性,請繼續(xù)關(guān)注該博客,后面會講PostSharp的多播特性。在介紹多播特性之前,我們?yōu)榱撕唵蜗仁褂脝蝹€特性。
現(xiàn)在,我們已經(jīng)寫了一個切面,并告訴PostSharp在那里使用它,以及PostSharp已經(jīng)執(zhí)行了編織。這個簡單的例子也許吸引不了你,但是注意你沒有對MyMethod本身做任何修改,就可以把代碼放到它的周圍,當然,要使用[MyAspect]特性才行。此外,使用特性并不是使用AOP的唯一方式:例如Castle DynamicProxy使用了IoC工具,這個后面再講。
小結(jié)
AOP并沒有聽上去那么復(fù)雜,你可能需要花費點時間來習(xí)慣,因為你可能必須要調(diào)整思考橫切關(guān)注點的方式。
AOP是一個鼓舞人心的、強大的工具,并且使用起來很有趣。本系列教程將使用的AOP工具是PostSharp和Castle DynamicProxy,如果你不喜歡,你可以選擇其他的AOP工具,見下表:
編譯時AOP工具
- PostSharp
- LinFu
- SheepAspect
- Fody
- CIL操作工具
運行時AOP工具
- Castle Windsor/DynamicProxy
- StructureMap
- Unity
- Spring.NET
最后,無論你選擇的是什么工具,AOP都會更加有效地完成工作:再也不用復(fù)制-粘貼相同的樣板代碼了或者在樣板代碼中修復(fù)相同的bug達到上百次。在抽象層面上,這會幫你有效地堅持單一職責(zé)原則和 開閉原則。在真實項目中,你會將更多的時間花在增值的功能上而不是那些乏味的工作上??傊?,掌握了AOP,會讓你事半功倍,愛上Code!