面向方面編程

面向方面編程

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

基本介紹

  • 中文名:面向方面編程
  • 外文名:AOP
  • 屬性:語言
  • 平台:PC
簡介,利弊,比較,特點,小結,

簡介

在考慮對象及對象與其他對象的關係時,我們通常會想到繼承這個術語。例如,定義某一個抽象類— 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 是截獲,事實並非如此。但是,它確實運用了截獲來套用通知以及組合行為。有一些 .NET 代碼示例通過 ContextBoundObject 以一種 AOP 翻版風格說明截獲。可是用 ContextBoundObject 來說明截獲並不合適,因為使用這種方法的先決條件是所有需要進行截獲的類都必須從 ContextBoundObject繼承。像 ContextBoundObject 這樣帶有先決條件的 AOP 方法會帶來需求產生的負面影響,所以在 AOP 中被視為重方法,應該避免使用。重方法在系統中遺留的大量“足跡”會潛在地影響每個類,阻礙將來更改或修改系統的功能。
我創建了一個名為 Encase 的輕量型框架。用“輕量型”這個術語的意義是整體上對系統沒有影響。系統的不同部分仍然受 AOP 影響,但是選擇輕量型框架並套用良好的編程實踐可以減輕大部分負面問題。Encase 框架的用途是簡化切點、混入和方面組合。開發人員能夠通過代碼在 Encase 中套用方面,從而代替大多數其他輕量型 AOP框架使用的配置檔案(例如 XML)。
重量型框架阻礙了 AOP 的套用,但是妨礙 AOP 廣泛套用的罪魁禍首是目前可用的 AOP 示例幾乎都都包含以下內容:執行方法前先截獲,並套用執行 Trace.WriteLine("Method entered.") 的方面。與普遍看法相反,除了日誌記錄、安全、規範以及這類性質的事情外,AOP 對於解決其他問題也很有用。

比較

為了說明更實用的使用 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 Sub
End Class
請查看 TheAgileDeveloper.Client 項目中的 Reference.vb 檔案。它是在導入 ContactService 的 Web 引用時通過 wsdl.exe 創建的。它從 WSDL 自動生成以下 Contact 類。
'<remarks/>
<System.Xml.Serialization.XmlTypeAttribute(_
[Namespace]:=http://。。。。。/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 Interface
Public Class ContactSave
Implements ISavable
Public Contact As ContactService.Contact
Public Sub Save() Implements ISavable.Save
ServiceManager.SaveContact(Me.Contact)
End Sub
End Class
在我們的應用程式中,混入 Contact對象中 ContactSave 實現的適當位置是 ServiceManager。我們能夠混入這個行為,但是不更改任何客戶端代碼(即,MainForm),因為套用混入後,結合 Contact 和 ContactSave 的新 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 object
contacts(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 圖表圖 2. 包裝對象的 UML 圖表
圖 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 時都會恢復為上一次接收更改的狀態。如果該接口是在一個部分類中實現的,那么在每個希望包含該行為的類中,都必須不厭其煩地重複實現這三個方法。作為一個實用主義編程人員,我嘗試堅持“一次且僅一次代碼”原則,所以我永遠不想重複任何代碼,複製和貼上越少越好。通過使用混入,我能夠重用實現 IUndoable 的 FieldUndoer對象。在 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 object
contacts(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 contacts
End 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 method
encaser.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.。。。 — 流行的 Java Spring 框架的一個 .NET 版本。在下一個版本中將實現 AOP。
Eos — 用於 C# 的一個面向方面的擴展。

小結

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

相關詞條

熱門詞條

聯絡我們