.net Aop 示例

這篇博客覆蓋的內(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)了IActionFilterAttribute類。這些特性可以應(yīng)用于action方法,它們會在action方法執(zhí)行前后運行(分別是OnActionExecuting和OnActionExecuted)。如果在一個新的ASP.NET MVC項目中,使用了默認的AccountController,那么你很可能已經(jīng)看到了action方法上的[Authorize]特性。AuthorizeAtrributeIActionFilter的內(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!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容