面向方面的編程
發(fā)布日期: 12/26/2005 | 更新日期: 12/26/2005
Matthew Deiters
ThoughtWorks
適用于:
Microsoft Visual Studio
Microsoft Visual Basic
摘要:從實(shí)際應(yīng)用的角度考察面向方面的編程,說(shuō)明如何動(dòng)態(tài)擴(kuò)展 Web 服務(wù)客戶端應(yīng)用程序中的行為。
單擊此處可下載本文的代碼示例。
本頁(yè)內(nèi)容
簡(jiǎn)介
面向方面的編程 (AOP) 由來(lái)已久,但是直到最近才開(kāi)始獲得 Microsoft .NET 開(kāi)發(fā)社區(qū)的青睞。任何一項(xiàng)新技術(shù)的采納往往都會(huì)產(chǎn)生對(duì)該技術(shù)及其使用的誤解,AOP 也不例外。為了澄清對(duì) AOP 的誤解,本文以及下列代碼示例將舉例說(shuō)明一個(gè) AOP 的實(shí)際應(yīng)用程序和一些 AOP 能夠解決的常見(jiàn)問(wèn)題。以使用 Web 服務(wù)的應(yīng)用程序?yàn)槔覀儗U(kuò)展該 Web 服務(wù)返回的對(duì)象功能,方法是通過(guò)一個(gè) AOP 框架對(duì)返回的對(duì)象應(yīng)用新的方面。這些方面將為此功能獨(dú)立生成對(duì)象模型,從而脫離 WSDL。
什么是方面?
在考慮對(duì)象及對(duì)象與其他對(duì)象的關(guān)系時(shí),我們通常會(huì)想到繼承這個(gè)術(shù)語(yǔ)。例如,定義某一個(gè)抽象類 — Dog 類。在標(biāo)識(shí)相似的一些類但每個(gè)類又有各自的獨(dú)特行為時(shí),通常使用繼承來(lái)擴(kuò)展功能。舉例來(lái)說(shuō),如果標(biāo)識(shí)了 Poodle,則可以說(shuō)一個(gè) Poodle 是一個(gè) Dog,即 Poodle 繼承了 Dog。到此為止都似乎不錯(cuò),但是如果定義另一個(gè)以后標(biāo)識(shí)為 Obedient Dog 的獨(dú)特行為又會(huì)怎樣呢?當(dāng)然,不是所有的 Dogs 都很馴服,所以 Dog 類不能包含 obedience 行為。此外,如果要?jiǎng)?chuàng)建從 Dog 繼承的 Obedient Dog 類,那么 Poodle 放在這個(gè)層次結(jié)構(gòu)中的哪個(gè)位置合適呢?Poodle 是一個(gè) Dog,但是 Poodle 不一定 obedient;那么 Poodle 是繼承于 Dog 還是 Obedient Dog 呢?都不是,我們可以將馴服看作一個(gè)方面,將其應(yīng)用到任何一類馴服的 Dog,我們反對(duì)以不恰當(dāng)?shù)姆绞綇?qiáng)制將該行為放在 Dog 層次結(jié)構(gòu)中。
在軟件術(shù)語(yǔ)中,面向方面的編程能夠獨(dú)立于任何繼承層次結(jié)構(gòu)而應(yīng)用改變類或?qū)ο笮袨榈姆矫妗H缓螅谶\(yùn)行時(shí)或編譯時(shí)應(yīng)用這些方面。舉一個(gè)關(guān)于 AOP 的示例,然后進(jìn)行描述,說(shuō)明起來(lái)比較容易。首先,定義四個(gè)關(guān)鍵的 AOP 術(shù)語(yǔ),這很重要,因?yàn)槲覍⒎磸?fù)使用它們:
? |
接合點(diǎn) (Joinpoint) — 代碼中定義明確的可識(shí)別的點(diǎn)。 |
? |
切點(diǎn) (Pointcut) — 通過(guò)配置或編碼指定接合點(diǎn)的一種方法。 |
? |
通知 (Advice) — 表示需要執(zhí)行交叉切割動(dòng)作的一種方法 |
? |
混入 (Mixin) — 通過(guò)將一個(gè)類的實(shí)例混入目標(biāo)類的實(shí)例引入新行為。 |
為了更好地理解這些術(shù)語(yǔ),可以將接合點(diǎn)看作程序流中定義好的一點(diǎn)。說(shuō)明接合點(diǎn)的一個(gè)很好的示例是:在代碼調(diào)用一個(gè)方法時(shí),發(fā)生調(diào)用的那一點(diǎn)被認(rèn)為是一個(gè)接合點(diǎn)。切點(diǎn)用于指定或定義希望在程序流中截獲的接合點(diǎn)。切點(diǎn)還包含一個(gè)通知,該通知在到達(dá)接合點(diǎn)時(shí)發(fā)生。因此,如果在一個(gè)調(diào)用的特定方法上定義一個(gè)切點(diǎn),那么在調(diào)用該方法或接合點(diǎn)時(shí),AOP 框架將截獲該切點(diǎn),同時(shí)還將執(zhí)行切點(diǎn)的通知。通知有幾種類型,但是最常見(jiàn)的情況是將其看作要調(diào)用的另一個(gè)方法。在調(diào)用一個(gè)帶有切點(diǎn)的方法時(shí),要執(zhí)行的通知將是另一個(gè)要調(diào)用的方法。要調(diào)用的這個(gè)通知或方法可以是對(duì)象中被截獲的方法,也可以是混入的另一個(gè)對(duì)象中的方法。我們將在后面進(jìn)一步解釋混入。
AOP:利與弊
一種常見(jiàn)的誤解是認(rèn)為 AOP 是截獲,事實(shí)并非如此。但是,它確實(shí)運(yùn)用了截獲來(lái)應(yīng)用通知以及組合行為。有一些 .NET 代碼示例通過(guò) ContextBoundObject 以一種 AOP 翻版風(fēng)格說(shuō)明截獲。可是用 ContextBoundObject 來(lái)說(shuō)明截獲并不合適,因?yàn)槭褂眠@種方法的先決條件是所有需要進(jìn)行截獲的類都必須從 ContextBoundObject 繼承。像 ContextBoundObject 這樣帶有先決條件的 AOP 方法會(huì)帶來(lái)需求產(chǎn)生的負(fù)面影響,所以在 AOP 中被視為重方法,應(yīng)該避免使用。重方法在系統(tǒng)中遺留的大量“足跡”會(huì)潛在地影響每個(gè)類,阻礙將來(lái)更改或修改系統(tǒng)的功能。
我創(chuàng)建了一個(gè)名為 Encase 的輕量型框架。用“輕量型”這個(gè)術(shù)語(yǔ)的意義是整體上對(duì)系統(tǒng)沒(méi)有影響。系統(tǒng)的不同部分仍然受 AOP 影響,但是選擇輕量型框架并應(yīng)用良好的編程實(shí)踐可以減輕大部分負(fù)面問(wèn)題。Encase 框架的用途是簡(jiǎn)化切點(diǎn)、混入和方面組合。開(kāi)發(fā)人員能夠通過(guò)代碼在 Encase 中應(yīng)用方面,從而代替大多數(shù)其他輕量型 AOP 框架使用的配置文件(例如 XML)。
重量型框架阻礙了 AOP 的應(yīng)用,但是防礙 AOP 廣泛應(yīng)用的罪魁禍?zhǔn)资悄壳翱捎玫?AOP 示例幾乎都都包含以下內(nèi)容:執(zhí)行方法前先截獲,并應(yīng)用執(zhí)行 Trace.WriteLine("Method entered.") 的方面。與普遍看法相反,除了日志記錄、安全、規(guī)范以及這類性質(zhì)的事情外,AOP 對(duì)于解決其他問(wèn)題也很有用。
超越 Trace.WriteLine
為了說(shuō)明更實(shí)用的使用 AOP 的方法,我們將創(chuàng)建一個(gè)應(yīng)用程序,從名為 ContactService.Service 的 Web 服務(wù)接收 people 對(duì)象的集合。目前,在 .NET 開(kāi)發(fā)中使用 Web 服務(wù)的最常見(jiàn)方法是調(diào)用返回 XML 的 Web 服務(wù),該服務(wù)通過(guò)框架自動(dòng)反序列化為一個(gè)對(duì)象。這些對(duì)象僅包含數(shù)據(jù)而不包含任何行為。在 .NET Framework 2.0 中,通過(guò)使用 partial 關(guān)鍵字并創(chuàng)建行為,能夠?qū)@些自動(dòng)代碼生成的對(duì)象添加功能。但是在一些 Web 服務(wù)或代理對(duì)象之間重用某個(gè)特定行為時(shí)仍然存在一個(gè)問(wèn)題。如前所述,多數(shù)情況下,共享的公共行為將包含在一個(gè)抽象類中,其他所有類從該類繼承。但是,我們不能使 Web 服務(wù)對(duì)象繼承功能。借此良機(jī),通過(guò)這個(gè)問(wèn)題說(shuō)明 AOP 功能如何強(qiáng)大。
我們的應(yīng)用程序用于顯示聯(lián)系人信息。最初它的用途是顯示信息,但是現(xiàn)在需要添加某些行為。為了查看代碼示例,我們需要?jiǎng)?chuàng)建一個(gè)稱為 TheAgileDeveloper.ContactService 的虛擬目錄。該目錄必須指向 TheAgileDeveloper.ContactService 項(xiàng)目在本地計(jì)算機(jī)上的位置。
注 通過(guò) http://localhost/TheAgileDeveloper.ContactService 可以訪問(wèn)此項(xiàng)目,這一點(diǎn)很重要。
圖 1. 應(yīng)用程序屏幕快照。
應(yīng)用程序有一個(gè)視圖,它是一個(gè)名為 MainForm 的 WinForm,用于顯示左側(cè) ListView 中 Web 服務(wù)返回的聯(lián)系人對(duì)象。選定一個(gè)聯(lián)系人時(shí),名字、姓氏和 Web 頁(yè)將顯示在右側(cè)的文本框中。載入 MainForm 時(shí),它調(diào)用 ServiceManager 類來(lái)獲取聯(lián)系人信息。下列 ServiceManager 類乍看起來(lái)似乎沒(méi)有添加任何值,只不過(guò)在窗體和 Web 服務(wù)之間添加了另一層。但是,它的價(jià)值就在于提供了一個(gè)在 Web 服務(wù)中添加新功能的位置,而不用重復(fù)代碼。另一個(gè)優(yōu)點(diǎn)是,它將 Web 服務(wù)的“足跡”抽象出來(lái),并從整個(gè)應(yīng)用程序中移除出去。
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
請(qǐng)查看 TheAgileDeveloper.Client 項(xiàng)目中的 Reference.vb 文件。它是在導(dǎo)入 ContactService 的 Web 引用時(shí)通過(guò) wsdl.exe 創(chuàng)建的。它從 WSDL 自動(dòng)生成以下 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 對(duì)象目前只處理數(shù)據(jù),而且我們不想以任何方式編輯該代碼,因?yàn)?wsdl.exe 會(huì)為我們自動(dòng)生成,所以下一次生成時(shí)更改將丟失。我想引入行為,這樣就能夠通過(guò)調(diào)用名為 Save 的方法保存對(duì)象,這很容易通過(guò)一個(gè)混入 來(lái)完成。混入 是多繼承的翻版,只是它有局限性,例如只能混入接口實(shí)現(xiàn)。我們使用的 Encase 框架包含一個(gè) Encaser 類,它負(fù)責(zé)接收并包裝一個(gè)對(duì)象。包裝對(duì)象的行為實(shí)際上意味著創(chuàng)建新的對(duì)象,在本例中就是新的 Contact 對(duì)象,它包含配置的混入和切點(diǎn)。
為了創(chuàng)建允許在 Contact 對(duì)象上調(diào)用 Save 方法的混入,需要指定一個(gè)接口,我稱之為 ISavable。實(shí)際混入對(duì)象的就是 ISavable 接口。我們需要在另一個(gè)稱為 ContactSave 的新類中實(shí)現(xiàn)該接口。
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
在我們的應(yīng)用程序中,混入 Contact 對(duì)象中 ContactSave 實(shí)現(xiàn)的適當(dāng)位置是 ServiceManager。我們能夠混入這個(gè)行為,但是不更改任何客戶端代碼(即,MainForm),因?yàn)閼?yīng)用混入后,結(jié)合 Contact 和 ContactSave 的新 Contact 對(duì)象仍然保持為最初的 Contact 類型。以下代碼是經(jīng)過(guò)更改的 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
混入的后臺(tái)實(shí)現(xiàn)
每個(gè)框架應(yīng)用切點(diǎn)、通知或方面的方法都是獨(dú)特的,但是其目的和概念是相同的。在本文示例中,Encaser 包裝一個(gè)對(duì)象時(shí)真正進(jìn)行的操作是,通過(guò) System.Reflection.Emit 命名空間中的類產(chǎn)生 MSIL 代碼,從而隨時(shí)創(chuàng)建新的 Contact 類型。新 Contact 類型派生于 Contact 類,它仍然共享類型,但是新包裝的對(duì)象還持有對(duì) ContactSave 對(duì)象的引用,后者是我們混入的。ISavable.Save 方法在新的 Contact 對(duì)象上實(shí)現(xiàn),因此在調(diào)用 Save 時(shí),它實(shí)際上將調(diào)用委托給混入的 ContactSave 對(duì)象。這樣做的優(yōu)點(diǎn)是能夠?qū)⑿碌?Contact 對(duì)象轉(zhuǎn)換為在任何混入對(duì)象上實(shí)現(xiàn)的任何接口。
圖 2. 包裝對(duì)象的 UML 圖表。
您或許在想,通過(guò) .NET Framework 2.0 的部分類語(yǔ)言功能,可以在另一個(gè) partial 類中添加 Save 行為。這是可能實(shí)現(xiàn)的,但是本文沒(méi)有采用這種方法,這是為了使代碼與 .NET Framework 1.x 的其他版本向后兼容。既然有部分語(yǔ)言功能,那么在正常情況下,前面的示例也就不需要使用混入 了。但是混入 仍然很有價(jià)值,因?yàn)橥ㄟ^(guò)它,開(kāi)發(fā)人員可以混入可重用的對(duì)象行為,這些對(duì)象可以源自其他不相關(guān)的對(duì)象層次結(jié)構(gòu),它實(shí)現(xiàn)的功能比 partial 類更多。在使用 partial 關(guān)鍵字時(shí),是在同一個(gè)類或類型中添加代碼,只不過(guò)物理位置不同。下一個(gè)混入示例說(shuō)明的添加行為不只特定于 Contact 類,而是一個(gè)名為 FieldUndoer 的可重用類。FieldUndoer 實(shí)現(xiàn)了 IUndoable 接口,允許已修改的對(duì)象恢復(fù)為原來(lái)的狀態(tài)。
Public Interface IUndoable
ReadOnly Property HasChanges() As Boolean
Sub Undo()
Sub AcceptChanges()
End Interface
HasChanges 屬性表示,如果發(fā)生了更改,Undo 將對(duì)象恢復(fù)為原來(lái)的狀態(tài),AcceptChanges 接收對(duì)象的當(dāng)前更改,因此任何時(shí)候再調(diào)用 Undo 時(shí)都會(huì)恢復(fù)為上一次接收更改的狀態(tài)。如果該接口是在一個(gè)部分類中實(shí)現(xiàn)的,那么在每個(gè)希望包含該行為的類中,都必須不厭其煩地重復(fù)實(shí)現(xiàn)這三個(gè)方法。作為一個(gè)實(shí)用主義編程人員,我嘗試堅(jiān)持“一次且僅一次代碼”原則,所以我永遠(yuǎn)不想重復(fù)任何代碼,復(fù)制和粘貼越少越好。通過(guò)使用混入,我能夠重用實(shí)現(xiàn) IUndoable 的 FieldUndoer 對(duì)象。在 ServiceManager 中我又混入了這個(gè)新功能。所有客戶端代碼仍然不知道新的混入,而且也不需要更改,除非需要使用 IUndoable 接口。更改 MainForm 中的 Contact 對(duì)象,然后單擊“撤消”,測(cè)試這個(gè)行為。
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 對(duì)象為例,在調(diào)用 ISavable.Save 方法時(shí),客戶端代碼還需要調(diào)用 IUndoable.AcceptChanges 方法,以便在下一次調(diào)用 IUndoable.Undo 時(shí)恢復(fù)到所保存的上一次更改。在這個(gè)小的 MainForm 中瀏覽和添加該對(duì)象很容易,但是在任何比用戶界面大得多的系統(tǒng)中對(duì)該規(guī)則編碼將是一項(xiàng)繁重的任務(wù)。您需要查找所有調(diào)用 Save 方法的情況,然后添加另一個(gè)對(duì) AcceptChanges 的調(diào)用。而且在創(chuàng)建新代碼的過(guò)程中,開(kāi)發(fā)人員也需要牢記,在每次調(diào)用 Save 時(shí)都添加這個(gè)新功能。這很快就會(huì)產(chǎn)生級(jí)聯(lián)效應(yīng),很容易會(huì)破壞系統(tǒng)穩(wěn)定姓,引入一些難于跟蹤的 bug。而使用面向方面的編程則能夠組合這些方法。指定一個(gè)切點(diǎn)和通知,在調(diào)用 Save 方法時(shí),Contact 對(duì)象將自動(dòng)調(diào)用后臺(tái)的 AcceptChanges。
為了在應(yīng)用程序中實(shí)現(xiàn)組合,需要在 ServiceManager 中再添加一行代碼。我們?cè)诩尤?FieldUndoer 混入后添加這行代碼。
'//Specify join point save, execute the AcceptChanges method
encaser.AddPointcut("Save", "AcceptChanges")
AddPointcut 方法通過(guò)幾個(gè)不同的簽名進(jìn)行重載,這為指定切點(diǎn)提供了更大的靈活性。我們調(diào)用的 AddPointcut 接收了一個(gè)字符串類型的接合點(diǎn)名,它表示為 Save 方法,然后又接收了一個(gè)名為 AcceptChanges 的方法作為執(zhí)行的通知。要查看這是否起作用,可以分別在 FieldUndoer.AcceptChanges 方法和 ContactSave.Save 方法前設(shè)置一個(gè)斷點(diǎn)。單擊 MainForm 上的 Save 按鈕將截獲接合點(diǎn),您首先將中斷至通知 — 即 AcceptChanges 方法。通知執(zhí)行后將執(zhí)行 Save 方法。
這個(gè)簡(jiǎn)單的示例說(shuō)明如何添加貫穿整個(gè)應(yīng)用程序的新行為,其功能強(qiáng)大無(wú)比。盡管有此功能,但它不僅僅是添加功能的一種很好的新方法。在眾多優(yōu)點(diǎn)中,只有幾個(gè)涉及代碼重用,以及通過(guò)簡(jiǎn)化新需求帶來(lái)的系統(tǒng)進(jìn)化來(lái)改進(jìn)系統(tǒng)的可維護(hù)性。與此同時(shí),誤用 AOP 會(huì)對(duì)系統(tǒng)的可維護(hù)性造成顯著的負(fù)面效應(yīng),因此了解使用 AOP 的時(shí)機(jī)和方法很重要。
AOP 走了多遠(yuǎn)?
將 AOP 用于多數(shù)大型系統(tǒng)或關(guān)鍵的生產(chǎn)系統(tǒng)還不完全成熟,但是隨著語(yǔ)言支持的提高,AOP 的應(yīng)用將更容易。另外,提高支持也是新的軟件開(kāi)發(fā)范例,例如利用面向方面的編程的軟件工廠。目前在 .NET 領(lǐng)域中有幾種可用的 AOP 框架,每個(gè)框架都有其自己的方法、正面屬性和負(fù)面屬性。
? |
Encase — 本代碼示例中的 Encase 框架只是一個(gè)工具,幫助您快速了解并運(yùn)行 AOP,以及理解 AOP 背后的概念。Encase 在運(yùn)行時(shí)期間應(yīng)用能夠單獨(dú)添加到對(duì)象的方面。 |
? |
Aspect# — 一個(gè)針對(duì) CLI 的 AOP 聯(lián)合兼容框架,提供聲明和配置方面的內(nèi)置語(yǔ)言。 |
? |
RAIL — RAIL 框架在虛擬機(jī) JIT 類時(shí)應(yīng)用方面。 |
? |
Spring.NET — 流行的 Java Spring 框架的一個(gè) .NET 版本。在下一個(gè)版本中將實(shí)現(xiàn) AOP。 |
? |
Eos — 用于 C# 的一個(gè)面向方面的擴(kuò)展。 |
小結(jié)
本文的目的是說(shuō)明一種比常規(guī)日志記錄或安全實(shí)例更實(shí)用的應(yīng)用 AOP 的新方法。正確應(yīng)用使用 AOP 會(huì)帶來(lái)很多優(yōu)點(diǎn),甚至能夠幫助您完成常規(guī)編程選項(xiàng)所不能完成的成果任務(wù)。我強(qiáng)烈推薦您在 internet 上搜尋大量可用資源,以指導(dǎo)應(yīng)用 AOP 的方法和場(chǎng)景時(shí)機(jī)。
關(guān)于作者
Matthew Deiters 對(duì)于軟件開(kāi)發(fā)工作充滿熱情,他是 ThoughtWorks 的一名咨詢?nèi)藛T。他曾協(xié)助通過(guò) .NET Framework 開(kāi)發(fā)一些針對(duì)金融和保險(xiǎn)行業(yè)的企業(yè)級(jí)系統(tǒng)。他看重 XP 編程和 TTD 方法論,認(rèn)為大多數(shù)人為問(wèn)題能夠通過(guò)設(shè)計(jì)模式和/或良好的單元測(cè)試解決。您可以通過(guò) Matthew 的個(gè)人 Web 空間與他聯(lián)系:www.theAgileDeveloper.com。
轉(zhuǎn)到原英文頁(yè)面
面向方面的編程
發(fā)布日期: 12/26/2005 | 更新日期: 12/26/2005
Matthew Deiters
ThoughtWorks
適用于:
Microsoft Visual Studio
Microsoft Visual Basic
摘要:從實(shí)際應(yīng)用的角度考察面向方面的編程,說(shuō)明如何動(dòng)態(tài)擴(kuò)展 Web 服務(wù)客戶端應(yīng)用程序中的行為。
單擊此處可下載本文的代碼示例。
本頁(yè)內(nèi)容
簡(jiǎn)介
面向方面的編程 (AOP) 由來(lái)已久,但是直到最近才開(kāi)始獲得 Microsoft .NET 開(kāi)發(fā)社區(qū)的青睞。任何一項(xiàng)新技術(shù)的采納往往都會(huì)產(chǎn)生對(duì)該技術(shù)及其使用的誤解,AOP 也不例外。為了澄清對(duì) AOP 的誤解,本文以及下列代碼示例將舉例說(shuō)明一個(gè) AOP 的實(shí)際應(yīng)用程序和一些 AOP 能夠解決的常見(jiàn)問(wèn)題。以使用 Web 服務(wù)的應(yīng)用程序?yàn)槔覀儗U(kuò)展該 Web 服務(wù)返回的對(duì)象功能,方法是通過(guò)一個(gè) AOP 框架對(duì)返回的對(duì)象應(yīng)用新的方面。這些方面將為此功能獨(dú)立生成對(duì)象模型,從而脫離 WSDL。
什么是方面?
在考慮對(duì)象及對(duì)象與其他對(duì)象的關(guān)系時(shí),我們通常會(huì)想到繼承這個(gè)術(shù)語(yǔ)。例如,定義某一個(gè)抽象類 — Dog 類。在標(biāo)識(shí)相似的一些類但每個(gè)類又有各自的獨(dú)特行為時(shí),通常使用繼承來(lái)擴(kuò)展功能。舉例來(lái)說(shuō),如果標(biāo)識(shí)了 Poodle,則可以說(shuō)一個(gè) Poodle 是一個(gè) Dog,即 Poodle 繼承了 Dog。到此為止都似乎不錯(cuò),但是如果定義另一個(gè)以后標(biāo)識(shí)為 Obedient Dog 的獨(dú)特行為又會(huì)怎樣呢?當(dāng)然,不是所有的 Dogs 都很馴服,所以 Dog 類不能包含 obedience 行為。此外,如果要?jiǎng)?chuàng)建從 Dog 繼承的 Obedient Dog 類,那么 Poodle 放在這個(gè)層次結(jié)構(gòu)中的哪個(gè)位置合適呢?Poodle 是一個(gè) Dog,但是 Poodle 不一定 obedient;那么 Poodle 是繼承于 Dog 還是 Obedient Dog 呢?都不是,我們可以將馴服看作一個(gè)方面,將其應(yīng)用到任何一類馴服的 Dog,我們反對(duì)以不恰當(dāng)?shù)姆绞綇?qiáng)制將該行為放在 Dog 層次結(jié)構(gòu)中。
在軟件術(shù)語(yǔ)中,面向方面的編程能夠獨(dú)立于任何繼承層次結(jié)構(gòu)而應(yīng)用改變類或?qū)ο笮袨榈姆矫妗H缓螅谶\(yùn)行時(shí)或編譯時(shí)應(yīng)用這些方面。舉一個(gè)關(guān)于 AOP 的示例,然后進(jìn)行描述,說(shuō)明起來(lái)比較容易。首先,定義四個(gè)關(guān)鍵的 AOP 術(shù)語(yǔ),這很重要,因?yàn)槲覍⒎磸?fù)使用它們:
? |
接合點(diǎn) (Joinpoint) — 代碼中定義明確的可識(shí)別的點(diǎn)。 |
? |
切點(diǎn) (Pointcut) — 通過(guò)配置或編碼指定接合點(diǎn)的一種方法。 |
? |
通知 (Advice) — 表示需要執(zhí)行交叉切割動(dòng)作的一種方法 |
? |
混入 (Mixin) — 通過(guò)將一個(gè)類的實(shí)例混入目標(biāo)類的實(shí)例引入新行為。 |
為了更好地理解這些術(shù)語(yǔ),可以將接合點(diǎn)看作程序流中定義好的一點(diǎn)。說(shuō)明接合點(diǎn)的一個(gè)很好的示例是:在代碼調(diào)用一個(gè)方法時(shí),發(fā)生調(diào)用的那一點(diǎn)被認(rèn)為是一個(gè)接合點(diǎn)。切點(diǎn)用于指定或定義希望在程序流中截獲的接合點(diǎn)。切點(diǎn)還包含一個(gè)通知,該通知在到達(dá)接合點(diǎn)時(shí)發(fā)生。因此,如果在一個(gè)調(diào)用的特定方法上定義一個(gè)切點(diǎn),那么在調(diào)用該方法或接合點(diǎn)時(shí),AOP 框架將截獲該切點(diǎn),同時(shí)還將執(zhí)行切點(diǎn)的通知。通知有幾種類型,但是最常見(jiàn)的情況是將其看作要調(diào)用的另一個(gè)方法。在調(diào)用一個(gè)帶有切點(diǎn)的方法時(shí),要執(zhí)行的通知將是另一個(gè)要調(diào)用的方法。要調(diào)用的這個(gè)通知或方法可以是對(duì)象中被截獲的方法,也可以是混入的另一個(gè)對(duì)象中的方法。我們將在后面進(jìn)一步解釋混入。
AOP:利與弊
一種常見(jiàn)的誤解是認(rèn)為 AOP 是截獲,事實(shí)并非如此。但是,它確實(shí)運(yùn)用了截獲來(lái)應(yīng)用通知以及組合行為。有一些 .NET 代碼示例通過(guò) ContextBoundObject 以一種 AOP 翻版風(fēng)格說(shuō)明截獲。可是用 ContextBoundObject 來(lái)說(shuō)明截獲并不合適,因?yàn)槭褂眠@種方法的先決條件是所有需要進(jìn)行截獲的類都必須從 ContextBoundObject 繼承。像 ContextBoundObject 這樣帶有先決條件的 AOP 方法會(huì)帶來(lái)需求產(chǎn)生的負(fù)面影響,所以在 AOP 中被視為重方法,應(yīng)該避免使用。重方法在系統(tǒng)中遺留的大量“足跡”會(huì)潛在地影響每個(gè)類,阻礙將來(lái)更改或修改系統(tǒng)的功能。
我創(chuàng)建了一個(gè)名為 Encase 的輕量型框架。用“輕量型”這個(gè)術(shù)語(yǔ)的意義是整體上對(duì)系統(tǒng)沒(méi)有影響。系統(tǒng)的不同部分仍然受 AOP 影響,但是選擇輕量型框架并應(yīng)用良好的編程實(shí)踐可以減輕大部分負(fù)面問(wèn)題。Encase 框架的用途是簡(jiǎn)化切點(diǎn)、混入和方面組合。開(kāi)發(fā)人員能夠通過(guò)代碼在 Encase 中應(yīng)用方面,從而代替大多數(shù)其他輕量型 AOP 框架使用的配置文件(例如 XML)。
重量型框架阻礙了 AOP 的應(yīng)用,但是防礙 AOP 廣泛應(yīng)用的罪魁禍?zhǔn)资悄壳翱捎玫?AOP 示例幾乎都都包含以下內(nèi)容:執(zhí)行方法前先截獲,并應(yīng)用執(zhí)行 Trace.WriteLine("Method entered.") 的方面。與普遍看法相反,除了日志記錄、安全、規(guī)范以及這類性質(zhì)的事情外,AOP 對(duì)于解決其他問(wèn)題也很有用。
超越 Trace.WriteLine
為了說(shuō)明更實(shí)用的使用 AOP 的方法,我們將創(chuàng)建一個(gè)應(yīng)用程序,從名為 ContactService.Service 的 Web 服務(wù)接收 people 對(duì)象的集合。目前,在 .NET 開(kāi)發(fā)中使用 Web 服務(wù)的最常見(jiàn)方法是調(diào)用返回 XML 的 Web 服務(wù),該服務(wù)通過(guò)框架自動(dòng)反序列化為一個(gè)對(duì)象。這些對(duì)象僅包含數(shù)據(jù)而不包含任何行為。在 .NET Framework 2.0 中,通過(guò)使用 partial 關(guān)鍵字并創(chuàng)建行為,能夠?qū)@些自動(dòng)代碼生成的對(duì)象添加功能。但是在一些 Web 服務(wù)或代理對(duì)象之間重用某個(gè)特定行為時(shí)仍然存在一個(gè)問(wèn)題。如前所述,多數(shù)情況下,共享的公共行為將包含在一個(gè)抽象類中,其他所有類從該類繼承。但是,我們不能使 Web 服務(wù)對(duì)象繼承功能。借此良機(jī),通過(guò)這個(gè)問(wèn)題說(shuō)明 AOP 功能如何強(qiáng)大。
我們的應(yīng)用程序用于顯示聯(lián)系人信息。最初它的用途是顯示信息,但是現(xiàn)在需要添加某些行為。為了查看代碼示例,我們需要?jiǎng)?chuàng)建一個(gè)稱為 TheAgileDeveloper.ContactService 的虛擬目錄。該目錄必須指向 TheAgileDeveloper.ContactService 項(xiàng)目在本地計(jì)算機(jī)上的位置。
注 通過(guò) http://localhost/TheAgileDeveloper.ContactService 可以訪問(wèn)此項(xiàng)目,這一點(diǎn)很重要。
圖 1. 應(yīng)用程序屏幕快照。
應(yīng)用程序有一個(gè)視圖,它是一個(gè)名為 MainForm 的 WinForm,用于顯示左側(cè) ListView 中 Web 服務(wù)返回的聯(lián)系人對(duì)象。選定一個(gè)聯(lián)系人時(shí),名字、姓氏和 Web 頁(yè)將顯示在右側(cè)的文本框中。載入 MainForm 時(shí),它調(diào)用 ServiceManager 類來(lái)獲取聯(lián)系人信息。下列 ServiceManager 類乍看起來(lái)似乎沒(méi)有添加任何值,只不過(guò)在窗體和 Web 服務(wù)之間添加了另一層。但是,它的價(jià)值就在于提供了一個(gè)在 Web 服務(wù)中添加新功能的位置,而不用重復(fù)代碼。另一個(gè)優(yōu)點(diǎn)是,它將 Web 服務(wù)的“足跡”抽象出來(lái),并從整個(gè)應(yīng)用程序中移除出去。
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
請(qǐng)查看 TheAgileDeveloper.Client 項(xiàng)目中的 Reference.vb 文件。它是在導(dǎo)入 ContactService 的 Web 引用時(shí)通過(guò) wsdl.exe 創(chuàng)建的。它從 WSDL 自動(dòng)生成以下 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 對(duì)象目前只處理數(shù)據(jù),而且我們不想以任何方式編輯該代碼,因?yàn)?wsdl.exe 會(huì)為我們自動(dòng)生成,所以下一次生成時(shí)更改將丟失。我想引入行為,這樣就能夠通過(guò)調(diào)用名為 Save 的方法保存對(duì)象,這很容易通過(guò)一個(gè)混入 來(lái)完成。混入 是多繼承的翻版,只是它有局限性,例如只能混入接口實(shí)現(xiàn)。我們使用的 Encase 框架包含一個(gè) Encaser 類,它負(fù)責(zé)接收并包裝一個(gè)對(duì)象。包裝對(duì)象的行為實(shí)際上意味著創(chuàng)建新的對(duì)象,在本例中就是新的 Contact 對(duì)象,它包含配置的混入和切點(diǎn)。
為了創(chuàng)建允許在 Contact 對(duì)象上調(diào)用 Save 方法的混入,需要指定一個(gè)接口,我稱之為 ISavable。實(shí)際混入對(duì)象的就是 ISavable 接口。我們需要在另一個(gè)稱為 ContactSave 的新類中實(shí)現(xiàn)該接口。
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
在我們的應(yīng)用程序中,混入 Contact 對(duì)象中 ContactSave 實(shí)現(xiàn)的適當(dāng)位置是 ServiceManager。我們能夠混入這個(gè)行為,但是不更改任何客戶端代碼(即,MainForm),因?yàn)閼?yīng)用混入后,結(jié)合 Contact 和 ContactSave 的新 Contact 對(duì)象仍然保持為最初的 Contact 類型。以下代碼是經(jīng)過(guò)更改的 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
混入的后臺(tái)實(shí)現(xiàn)
每個(gè)框架應(yīng)用切點(diǎn)、通知或方面的方法都是獨(dú)特的,但是其目的和概念是相同的。在本文示例中,Encaser 包裝一個(gè)對(duì)象時(shí)真正進(jìn)行的操作是,通過(guò) System.Reflection.Emit 命名空間中的類產(chǎn)生 MSIL 代碼,從而隨時(shí)創(chuàng)建新的 Contact 類型。新 Contact 類型派生于 Contact 類,它仍然共享類型,但是新包裝的對(duì)象還持有對(duì) ContactSave 對(duì)象的引用,后者是我們混入的。ISavable.Save 方法在新的 Contact 對(duì)象上實(shí)現(xiàn),因此在調(diào)用 Save 時(shí),它實(shí)際上將調(diào)用委托給混入的 ContactSave 對(duì)象。這樣做的優(yōu)點(diǎn)是能夠?qū)⑿碌?Contact 對(duì)象轉(zhuǎn)換為在任何混入對(duì)象上實(shí)現(xiàn)的任何接口。
圖 2. 包裝對(duì)象的 UML 圖表。
您或許在想,通過(guò) .NET Framework 2.0 的部分類語(yǔ)言功能,可以在另一個(gè) partial 類中添加 Save 行為。這是可能實(shí)現(xiàn)的,但是本文沒(méi)有采用這種方法,這是為了使代碼與 .NET Framework 1.x 的其他版本向后兼容。既然有部分語(yǔ)言功能,那么在正常情況下,前面的示例也就不需要使用混入 了。但是混入 仍然很有價(jià)值,因?yàn)橥ㄟ^(guò)它,開(kāi)發(fā)人員可以混入可重用的對(duì)象行為,這些對(duì)象可以源自其他不相關(guān)的對(duì)象層次結(jié)構(gòu),它實(shí)現(xiàn)的功能比 partial 類更多。在使用 partial 關(guān)鍵字時(shí),是在同一個(gè)類或類型中添加代碼,只不過(guò)物理位置不同。下一個(gè)混入示例說(shuō)明的添加行為不只特定于 Contact 類,而是一個(gè)名為 FieldUndoer 的可重用類。FieldUndoer 實(shí)現(xiàn)了 IUndoable 接口,允許已修改的對(duì)象恢復(fù)為原來(lái)的狀態(tài)。
Public Interface IUndoable
ReadOnly Property HasChanges() As Boolean
Sub Undo()
Sub AcceptChanges()
End Interface
HasChanges 屬性表示,如果發(fā)生了更改,Undo 將對(duì)象恢復(fù)為原來(lái)的狀態(tài),AcceptChanges 接收對(duì)象的當(dāng)前更改,因此任何時(shí)候再調(diào)用 Undo 時(shí)都會(huì)恢復(fù)為上一次接收更改的狀態(tài)。如果該接口是在一個(gè)部分類中實(shí)現(xiàn)的,那么在每個(gè)希望包含該行為的類中,都必須不厭其煩地重復(fù)實(shí)現(xiàn)這三個(gè)方法。作為一個(gè)實(shí)用主義編程人員,我嘗試堅(jiān)持“一次且僅一次代碼”原則,所以我永遠(yuǎn)不想重復(fù)任何代碼,復(fù)制和粘貼越少越好。通過(guò)使用混入,我能夠重用實(shí)現(xiàn) IUndoable 的 FieldUndoer 對(duì)象。在 ServiceManager 中我又混入了這個(gè)新功能。所有客戶端代碼仍然不知道新的混入,而且也不需要更改,除非需要使用 IUndoable 接口。更改 MainForm 中的 Contact 對(duì)象,然后單擊“撤消”,測(cè)試這個(gè)行為。
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 對(duì)象為例,在調(diào)用 ISavable.Save 方法時(shí),客戶端代碼還需要調(diào)用 IUndoable.AcceptChanges 方法,以便在下一次調(diào)用 IUndoable.Undo 時(shí)恢復(fù)到所保存的上一次更改。在這個(gè)小的 MainForm 中瀏覽和添加該對(duì)象很容易,但是在任何比用戶界面大得多的系統(tǒng)中對(duì)該規(guī)則編碼將是一項(xiàng)繁重的任務(wù)。您需要查找所有調(diào)用 Save 方法的情況,然后添加另一個(gè)對(duì) AcceptChanges 的調(diào)用。而且在創(chuàng)建新代碼的過(guò)程中,開(kāi)發(fā)人員也需要牢記,在每次調(diào)用 Save 時(shí)都添加這個(gè)新功能。這很快就會(huì)產(chǎn)生級(jí)聯(lián)效應(yīng),很容易會(huì)破壞系統(tǒng)穩(wěn)定姓,引入一些難于跟蹤的 bug。而使用面向方面的編程則能夠組合這些方法。指定一個(gè)切點(diǎn)和通知,在調(diào)用 Save 方法時(shí),Contact 對(duì)象將自動(dòng)調(diào)用后臺(tái)的 AcceptChanges。
為了在應(yīng)用程序中實(shí)現(xiàn)組合,需要在 ServiceManager 中再添加一行代碼。我們?cè)诩尤?FieldUndoer 混入后添加這行代碼。
'//Specify join point save, execute the AcceptChanges method
encaser.AddPointcut("Save", "AcceptChanges")
AddPointcut 方法通過(guò)幾個(gè)不同的簽名進(jìn)行重載,這為指定切點(diǎn)提供了更大的靈活性。我們調(diào)用的 AddPointcut 接收了一個(gè)字符串類型的接合點(diǎn)名,它表示為 Save 方法,然后又接收了一個(gè)名為 AcceptChanges 的方法作為執(zhí)行的通知。要查看這是否起作用,可以分別在 FieldUndoer.AcceptChanges 方法和 ContactSave.Save 方法前設(shè)置一個(gè)斷點(diǎn)。單擊 MainForm 上的 Save 按鈕將截獲接合點(diǎn),您首先將中斷至通知 — 即 AcceptChanges 方法。通知執(zhí)行后將執(zhí)行 Save 方法。
這個(gè)簡(jiǎn)單的示例說(shuō)明如何添加貫穿整個(gè)應(yīng)用程序的新行為,其功能強(qiáng)大無(wú)比。盡管有此功能,但它不僅僅是添加功能的一種很好的新方法。在眾多優(yōu)點(diǎn)中,只有幾個(gè)涉及代碼重用,以及通過(guò)簡(jiǎn)化新需求帶來(lái)的系統(tǒng)進(jìn)化來(lái)改進(jìn)系統(tǒng)的可維護(hù)性。與此同時(shí),誤用 AOP 會(huì)對(duì)系統(tǒng)的可維護(hù)性造成顯著的負(fù)面效應(yīng),因此了解使用 AOP 的時(shí)機(jī)和方法很重要。
AOP 走了多遠(yuǎn)?
將 AOP 用于多數(shù)大型系統(tǒng)或關(guān)鍵的生產(chǎn)系統(tǒng)還不完全成熟,但是隨著語(yǔ)言支持的提高,AOP 的應(yīng)用將更容易。另外,提高支持也是新的軟件開(kāi)發(fā)范例,例如利用面向方面的編程的軟件工廠。目前在 .NET 領(lǐng)域中有幾種可用的 AOP 框架,每個(gè)框架都有其自己的方法、正面屬性和負(fù)面屬性。
? |
Encase — 本代碼示例中的 Encase 框架只是一個(gè)工具,幫助您快速了解并運(yùn)行 AOP,以及理解 AOP 背后的概念。Encase 在運(yùn)行時(shí)期間應(yīng)用能夠單獨(dú)添加到對(duì)象的方面。 |
? |
Aspect# — 一個(gè)針對(duì) CLI 的 AOP 聯(lián)合兼容框架,提供聲明和配置方面的內(nèi)置語(yǔ)言。 |
? |
RAIL — RAIL 框架在虛擬機(jī) JIT 類時(shí)應(yīng)用方面。 |
? |
Spring.NET — 流行的 Java Spring 框架的一個(gè) .NET 版本。在下一個(gè)版本中將實(shí)現(xiàn) AOP。 |
? |
Eos — 用于 C# 的一個(gè)面向方面的擴(kuò)展。 |
小結(jié)
本文的目的是說(shuō)明一種比常規(guī)日志記錄或安全實(shí)例更實(shí)用的應(yīng)用 AOP 的新方法。正確應(yīng)用使用 AOP 會(huì)帶來(lái)很多優(yōu)點(diǎn),甚至能夠幫助您完成常規(guī)編程選項(xiàng)所不能完成的成果任務(wù)。我強(qiáng)烈推薦您在 internet 上搜尋大量可用資源,以指導(dǎo)應(yīng)用 AOP 的方法和場(chǎng)景時(shí)機(jī)。
關(guān)于作者
Matthew Deiters 對(duì)于軟件開(kāi)發(fā)工作充滿熱情,他是 ThoughtWorks 的一名咨詢?nèi)藛T。他曾協(xié)助通過(guò) .NET Framework 開(kāi)發(fā)一些針對(duì)金融和保險(xiǎn)行業(yè)的企業(yè)級(jí)系統(tǒng)。他看重 XP 編程和 TTD 方法論,認(rèn)為大多數(shù)人為問(wèn)題能夠通過(guò)設(shè)計(jì)模式和/或良好的單元測(cè)試解決。您可以通過(guò) Matthew 的個(gè)人 Web 空間與他聯(lián)系:www.theAgileDeveloper.com。
轉(zhuǎn)到原英文頁(yè)面