面向方面的編程

來源:互聯網
上載者:User
Matthew Deiters
ThoughtWorks

適用於:
Microsoft Visual Studio
Microsoft Visual Basic

摘要:從實際應用的角度考察面向方面的編程,說明如何動態擴充 Web 服務用戶端應用程式中的行為。

單擊此處可下載本文的程式碼範例。

本頁內容
簡介
什麼是方面?
AOP:利與弊
超越 Trace.WriteLine
混入的後台實現
組合行為
AOP 走了多遠?
小結


簡介

面向方面的編程 (AOP) 由來已久,但是直到最近才開始獲得 Microsoft .NET 開發社區的青睞。任何一項新技術的採納往往都會產生對該技術及其使用的誤解,AOP 也不例外。為了澄清對 AOP 的誤解,本文以及下列程式碼範例將舉例說明一個 AOP 的實際應用程式和一些 AOP 能夠解決的常見問題。以使用 Web 服務的應用程式為例,我們將擴充該 Web 服務返回的對象功能,方法是通過一個 AOP 架構對返回的對象應用新的方面。這些方面將為此功能獨立產生物件模型,從而脫離 WSDL。

返回頁首


什麼是方面?

在考慮對象及對象與其他對象的關係時,我們通常會想到繼承這個術語。例如,定義某一個抽象類別 — Dog 類。在標識相似的一些類但每個類又有各自的獨特行為時,通常使用繼承來擴充功能。舉例來說,如果標識了 Poodle,則可以說一個 Poodle 是一個 Dog,即 Poodle 繼承了 Dog。到此為止都似乎不錯,但是如果定義另一個以後標識為 Obedient Dog 的獨特行為又會怎樣呢?當然,不是所有的 Dogs 都很馴服,所以 Dog 類不能包含 obedience 行為。此外,如果要建立從 Dog 繼承的 Obedient Dog 類,那麼 Poodle 放在這個階層中的哪個位置合適呢?Poodle 是一個 Dog,但是 Poodle 不一定 obedient;那麼 Poodle 是繼承於 Dog 還是 Obedient Dog 呢?都不是,我們可以將馴服看作一個方面,將其應用到任何一類馴服的 Dog,我們反對以不恰當的方式強制將該行為放在 Dog 階層中。

在軟體術語中,面向方面的編程能夠獨立於任何繼承階層而應用改變類或對象行為的方面。然後,在運行時或編譯時間應用這些方面。舉一個關於 AOP 的樣本,然後進行描述,說明起來比較容易。首先,定義四個關鍵的 AOP 術語,這很重要,因為我將反覆使用它們:

接合點 (Joinpoint) — 代碼中定義明確的可識別的點。

切點 (Pointcut) — 通過配置或編碼指定接合點的一種方法。

通知 (Advice) — 表示需要執行交叉切割動作的一種方法

混入 (Mixin) — 通過將一個類的執行個體混入目標類的執行個體引入新行為。

為了更好地理解這些術語,可以將接合點看作程式流中定義好的一點。說明接合點的一個很好的樣本是:在代碼調用一個方法時,發生調用的那一點被認為是一個接合點。切點用於指定或定義希望在程式流中截獲的接合點。切點還包含一個通知,該通知在到達接合點時發生。因此,如果在一個調用的特定方法上定義一個切點,那麼在調用該方法或接合點時,AOP 架構將截獲該切點,同時還將執行切點的通知。通知有幾種類型,但是最常見的情況是將其看作要調用的另一個方法。在調用一個帶有切點的方法時,要執行的通知將是另一個要調用的方法。要調用的這個通知或方法可以是對象中被截獲的方法,也可以是混入的另一個對象中的方法。我們將在後面進一步解釋混入。

返回頁首


AOP:利與弊

一種常見的誤解是認為 AOP 是截獲,事實並非如此。但是,它確實運用了截獲來應用通知以及組合行為。有一些 .NET 程式碼範例通過 ContextBoundObject 以一種 AOP 翻版風格說明截獲。可是用 ContextBoundObject 來說明截獲並不合適,因為使用這種方法的先決條件是所有需要進行截獲的類都必須從 ContextBoundObject 繼承。像 ContextBoundObject 這樣帶有先決條件的 AOP 方法會帶來需求產生的負面影響,所以在 AOP 中被視為重方法,應該避免使用。重方法在系統中遺留的大量“足跡”會潛在地影響每個類,阻礙將來更改或修改系統的功能。

我建立了一個名為 Encase 的輕量型架構。用“輕量型”這個術語的意義是整體上對系統沒有影響。系統的不同部分仍然受 AOP 影響,但是選擇輕量型架構並應用良好的編程實踐可以減輕大部分負面問題。Encase 架構的用途是簡化切點、混入和方面組合。開發人員能夠通過代碼在 Encase 中應用方面,從而代替大多數其他輕量型 AOP 架構使用的設定檔(例如 XML)。

重量型架構阻礙了 AOP 的應用,但是防礙 AOP 廣泛應用的罪魁禍首是目前可用的 AOP 樣本幾乎都都包含以下內容:執行方法前先截獲,並應用執行 Trace.WriteLine("Method entered.") 的方面。與普遍看法相反,除了日誌記錄、安全、規範以及這類性質的事情外,AOP 對於解決其他問題也很有用。

返回頁首


超越 Trace.WriteLine

為了說明更實用的使用 AOP 的方法,我們將建立一個應用程式,從名為 ContactService.Service 的 Web 服務接收 people 對象的集合。目前,在 .NET 開發中使用 Web 服務的最常見方法是調用返回 XML 的 Web 服務,該服務通過架構自動還原序列化為一個對象。這些對象僅包含資料而不包含任何行為。在 .NET Framework 2.0 中,通過使用 partial 關鍵字並建立行為,能夠對這些自動代碼產生的對象添加功能。但是在一些 Web 服務或代理對象之間重用某個特定行為時仍然存在一個問題。如前所述,多數情況下,共用的公用行為將包含在一個抽象類別中,其他所有類從該類繼承。但是,我們不能使 Web 服務對象繼承功能。藉此良機,通過這個問題說明 AOP 功能如何強大。

我們的應用程式用於顯示連絡人資訊。最初它的用途是顯示資訊,但是現在需要添加某些行為。為了查看程式碼範例,我們需要建立一個稱為 TheAgileDeveloper.ContactService 的虛擬目錄。該目錄必須指向 TheAgileDeveloper.ContactService 項目在本機電腦上的位置。

通過 http://localhost/TheAgileDeveloper.ContactService 可以訪問此項目,這一點很重要。

1. 應用程式螢幕快照。

應用程式有一個視圖,它是一個名為 MainForm 的 WinForm,用於顯示左側 ListView 中 Web 服務返回的連絡人對象。選定一個連絡人時,名字、姓氏和 Web 頁將顯示在右側的文字框中。載入 MainForm 時,它調用 ServiceManager 類來擷取連絡人資訊。下列 ServiceManager 類乍看起來似乎沒有添加任何值,只不過在表單和 Web 服務之間添加了另一層。但是,它的價值就在於提供了一個在 Web 服務中添加新功能的位置,而不用重複代碼。另一個優點是,它將 Web 服務的“足跡”抽象出來,並從整個應用程式中移除出去。

Public Class ServiceManager    Public Shared Function GetAllContacts() As ContactService.Contact()        Dim service As ContactService.Service = New ContactService.Service        Dim contacts() As ContactService.Contact = service.GetAllContacts        Return contacts    End Function    Public Shared Sub SaveContact(ByVal contact As ContactService.Contact)        Dim service As ContactService.Service = New ContactService.Service        service.SaveContact(contact)    End SubEnd Class

請查看 TheAgileDeveloper.Client 項目中的 Reference.vb 檔案。它是在匯入 ContactService 的 Web 參考時通過 wsdl.exe 建立的。它從 WSDL 自動產生以下 Contact 類。

'<remarks/>    <System.Xml.Serialization.XmlTypeAttribute(_  [Namespace]:=http://tempuri.org/TheAgileDeveloper.ContactService/Service1 _ )>  _    Public Class Contact                '<remarks/>        Public Id As Integer                '<remarks/>        Public FirstName As String                '<remarks/>        Public LastName As String                '<remarks/>        Public WebSite As String    End Class

注意,Contact 對象目前只處理資料,而且我們不想以任何方式編輯該代碼,因為 wsdl.exe 會為我們自動產生,所以下一次產生時更改將丟失。我想引入行為,這樣就能夠通過調用名為 Save 的方法儲存對象,這很容易通過一個混入 來完成。混入 是多繼承的翻版,只是它有局限性,例如只能混入介面實現。我們使用的 Encase 架構套件含一個 Encaser 類,它負責接收並封裝一個對象。封裝對象的行為實際上意味著建立新的對象,在本例中就是新的 Contact 對象,它包含配置的混入和切點。

為了建立允許在 Contact 對象上調用 Save 方法的混入,需要指定一個介面,我稱之為 ISavable。實際混入對象的就是 ISavable 介面。我們需要在另一個稱為 ContactSave 的新類中實現該介面。

Public Interface ISaveable    Sub Save()End InterfacePublic Class ContactSave    Implements ISavable    Public Contact As ContactService.Contact    Public Sub Save() Implements ISavable.Save        ServiceManager.SaveContact(Me.Contact)    End SubEnd Class

在我們的應用程式中,混入 Contact 對象中 ContactSave 實現的適當位置是 ServiceManager。我們能夠混入這個行為,但是不更改任何用戶端代碼(即,MainForm),因為應用混入後,結合 ContactContactSave 的新 Contact 對象仍然保持為最初的 Contact 類型。以下代碼是經過更改的 ServiceManager 的 GetAllContacts 方法,它處理混入行為。

Public Shared Function GetAllContacts() As ContactService.Contact()        Dim service As ContactService.Service = New ContactService.Service        Dim contacts() As ContactService.Contact = service.GetAllContacts        '//Wrap each contact object        For i As Integer = 0 To contacts.Length-1            '//Create a new instance of the '//encaser responsible for wrapping our object            Dim encaser As encaser = New encaser            '//Add mixin instance of ContactSave            Dim saver As ContactSave = New ContactSave            encaser.AddMixin(saver)            '//Creates a new object with '//Contact and ContactSave implementations            Dim wrappedObject As Object = encaser.Wrap(contacts(i))            '//Assign our new wrapped contact object '//to the previous contact objectcontacts(i) = DirectCast(wrappedObject, _  ContactService.Contact) '//Notice the wrapped object is still the same type            '//Assign the new wrapped Contact object to '//target field of the ContactSave mixed in            saver.Target = contacts(i)        Next        Return contacts    End Function
返回頁首


混入的後台實現

每個架構應用切點、通知或方面的方法都是獨特的,但是其目的和概念是相同的。在本文樣本中,Encaser 封裝一個對象時真正進行的操作是,通過 System.Reflection.Emit 命名空間中的類產生 MSIL 代碼,從而隨時建立新的 Contact 類型。新 Contact 類型派生於 Contact 類,它仍然共用類型,但是新封裝的對象還持有對 ContactSave 對象的引用,後者是我們混入的。ISavable.Save 方法在新的 Contact 對象上實現,因此在調用 Save 時,它實際上將調用委託給混入的 ContactSave 對象。這樣做的優點是能夠將新的 Contact 對象轉換為在任何混入對象上實現的任何介面。

2. 封裝對象的 UML 圖表。

您或許在想,通過 .NET Framework 2.0 的部分類語言功能,可以在另一個 partial 類中添加 Save 行為。這是可能實現的,但是本文沒有採用這種方法,這是為了使代碼與 .NET Framework 1.x 的其他版本向後相容。既然有部分語言功能,那麼在正常情況下,前面的樣本也就不需要使用混入 了。但是混入 仍然很有價值,因為通過它,開發人員可以混入可重用的對象行為,這些對象可以源自其他不相關的對象階層,它實現的功能比 partial 類更多。在使用 partial 關鍵字時,是在同一個類或類型中添加代碼,只不過物理位置不同。下一個混入樣本說明的添加行為不只特定於 Contact 類,而是一個名為 FieldUndoer 的可重用類。FieldUndoer 實現了 IUndoable 介面,允許已修改的對象恢複為原來的狀態。

    Public Interface IUndoable        ReadOnly Property HasChanges() As Boolean        Sub Undo()        Sub AcceptChanges()    End Interface

HasChanges 屬性工作表示,如果發生了更改,Undo 將對象恢複為原來的狀態,AcceptChanges 接收對象的當前更改,因此任何時候再調用 Undo 時都會恢複為上一次接收更改的狀態。如果該介面是在一個部分類中實現的,那麼在每個希望包含該行為的類中,都必須不厭其煩地重複實現這三個方法。作為一個實用主義編程人員,我嘗試堅持“一次且僅一次代碼”原則,所以我永遠不想重複任何代碼,複製和粘貼越少越好。通過使用混入,我能夠重用實現 IUndoableFieldUndoer 對象。在 ServiceManager 中我又混入了這個新功能。所有用戶端代碼仍然不知道新的混入,而且也不需要更改,除非需要使用 IUndoable 介面。更改 MainForm 中的 Contact 對象,然後單擊“撤消”,測試這個行為。

Public Shared Function GetAllContacts() As ContactService.Contact()        Dim service As ContactService.Service = New ContactService.Service        Dim contacts() As ContactService.Contact = service.GetAllContacts        '//Wrap each contact object        For i As Integer = 0 To contacts.Length-1            '//Create a new instance of the encaser '//responsible for wrapping our object            Dim encaser As encaser = New encaser            '//Add mixin instance of ContactSave            Dim saver As ContactSave = New ContactSave            encaser.AddMixin(saver)            '//Add mixin instance of FieldUndoer            Dim undoer As FieldUndoer = New FieldUndoer            encaser.AddMixin(undoer)            '//Creates a new object with Contact '//and ContactSave implementations            Dim wrappedObject As Object = encaser.Wrap(contacts(i))            '//Assign our new wrapped contact object '//to the previous contact objectcontacts(i) = DirectCast(wrappedObject, _ ContactService.Contact) '//Notice the wrapped object is still the same type            '//Assign the new wrapped Contact object to target fields             saver.Target = contacts(i)            undoer.Target = contacts(i)        Next        Return contactsEnd Function
返回頁首


組合行為

混入還只是冰山一角。真正讓 AOP 聲名鵲起的功能是組合混入行為。以使用新 Contact 對象為例,在調用 ISavable.Save 方法時,用戶端代碼還需要調用 IUndoable.AcceptChanges 方法,以便在下一次調用 IUndoable.Undo 時恢複到所儲存的上一次更改。在這個小的 MainForm 中瀏覽和添加該對象很容易,但是在任何比使用者介面大得多的系統中對該規則編碼將是一項繁重的任務。您需要尋找所有調用 Save 方法的情況,然後添加另一個對 AcceptChanges 的調用。而且在建立新代碼的過程中,開發人員也需要牢記,在每次調用 Save 時都添加這個新功能。這很快就會產生級聯效應,很容易會破壞系統穩定姓,引入一些難於跟蹤的 bug。而使用面向方面的編程則能夠組合這些方法。指定一個切點和通知,在調用 Save 方法時,Contact 對象將自動調用背景 AcceptChanges

為了在應用程式中實現組合,需要在 ServiceManager 中再添加一行代碼。我們在加入 FieldUndoer 混入後添加這行代碼。

'//Specify join point save, execute the AcceptChanges methodencaser.AddPointcut("Save", "AcceptChanges") 

AddPointcut 方法通過幾個不同的簽名進行重載,這為指定切點提供了更大的靈活性。我們調用的 AddPointcut 接收了一個字串類型的接合點名,它表示為 Save 方法,然後又接收了一個名為 AcceptChanges 的方法作為執行的通知。要查看這是否起作用,可以分別在 FieldUndoer.AcceptChanges 方法和 ContactSave.Save 方法前設定一個斷點。單擊 MainForm 上的 Save 按鈕將截獲接合點,您首先將中斷至通知 — 即 AcceptChanges 方法。通知執行後將執行 Save 方法。

這個簡單的樣本說明如何添加貫穿整個應用程式的新行為,其功能強大無比。儘管有此功能,但它不僅僅是添加功能的一種很好的新方法。在眾多優點中,只有幾個涉及代碼重用,以及通過簡化新需求帶來的系統進化來改進系統的可維護性。與此同時,誤用 AOP 會對系統的可維護性造成顯著的負面效應,因此瞭解使用 AOP 的時機和方法很重要。

返回頁首


AOP 走了多遠?

將 AOP 用於多數大型系統或關鍵的生產系統還不完全成熟,但是隨著語言支援的提高,AOP 的應用將更容易。另外,提高支援也是新的軟體開發範例,例如利用面向方面的編程的軟體工廠。目前在 .NET 領域中有幾種可用的 AOP 架構,每個架構都有其自己的方法、正面屬性和負面屬性。

Encase — 本程式碼範例中的 Encase 架構只是一個工具,協助您快速瞭解並運行 AOP,以及理解 AOP 背後的概念。Encase 在運行時期間應用能夠單獨添加到對象的方面。

Aspect# — 一個針對 CLI 的 AOP 聯合相容架構,提供聲明和配置方面的內建語言。

RAIL — RAIL 架構在虛擬機器 JIT 類時應用方面。

Spring.NET — 流行的 Java Spring 架構的一個 .NET 版本。在下一個版本中將實現 AOP。

Eos — 用於 C# 的一個面向方面的擴充。

返回頁首


小結

本文的目的是說明一種比常規日誌記錄或安全執行個體更實用的應用 AOP 的新方法。正確應用使用 AOP 會帶來很多優點,甚至能夠協助您完成常規編程選項所不能完成的成果任務。我強烈推薦您在 internet 上搜尋大量可用資源,以指導應用 AOP 的方法和情境時機。

關於作者

Matthew Deiters 對於軟體開發工作充滿熱情,他是 ThoughtWorks 的一名諮詢人員。他曾協助通過 .NET Framework 開發一些針對金融和保險行業的企業級系統。他看重 XP 編程和 TTD 方法論,認為大多數人為問題能夠通過設計模式和/或良好的單元測試解決。您可以通過 Matthew 的個人 Web 空間與他聯絡:www.theAgileDeveloper.com

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.