面向方面的编程

Matthew Deiters
ThoughtWorks

适用于:
Microsoft Visual Studio
Microsoft Visual Basic

摘要:从实际应用的角度考察面向方面的编程,说明如何动态扩展 Web 服务客户端应用程序中的行为。

单击此处可下载本文的代码示例。

*
本页内容
简介简介
什么是方面?什么是方面?
AOP:利与弊AOP:利与弊
超越 Trace.WriteLine超越 Trace.WriteLine
混入的后台实现混入的后台实现
组合行为组合行为
AOP 走了多远?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 Sub

End 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 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),因为应用混入后,结合 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 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 图表。

您或许在想,通过 .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 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.NET — 流行的 Java Spring 框架的一个 .NET 版本。在下一个版本中将实现 AOP。

Eos — 用于 C# 的一个面向方面的扩展。

小结

本文的目的是说明一种比常规日志记录或安全实例更实用的应用 AOP 的新方法。正确应用使用 AOP 会带来很多优点,甚至能够帮助您完成常规编程选项所不能完成的成果任务。我强烈推荐您在 internet 上搜寻大量可用资源,以指导应用 AOP 的方法和场景时机。

关于作者

Matthew Deiters 对于软件开发工作充满热情,他是 ThoughtWorks 的一名咨询人员。他曾协助通过 .NET Framework 开发一些针对金融和保险行业的企业级系统。他看重 XP 编程和 TTD 方法论,认为大多数人为问题能够通过设计模式和/或良好的单元测试解决。您可以通过 Matthew 的个人 Web 空间与他联系:www.theAgileDeveloper.com

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页