毫无疑问,面向对象是一种主流编程模式,当涉及到将某个系统分割为组件并通过组件来描述过程时,这种模式占有优势。 当处理某组件的业务特定关注点时,面向对象 (OO) 模式同样占有优势。 但是,当涉及到处理横切关注点时,OO 模式不再有效。 一般来说,横切关注点是一个在系统中影响多个组件的关注点。
为了最大限度地重用复杂的业务逻辑代码,您通常倾向于围绕系统的核心和主要业务功能设计类的层次结构。 但其他横切类层次结构的非业务特定关注点该如何实现? 缓存、安全和日志记录等功能在什么位置适合? 很可能就是在每个受影响的对象中重复使用这些功能。
横切关注点是必须在一个不同的逻辑级别(超出应用程序类范围的级别)处理的系统的一个方面,而不是给定组件或系列组件的特定职责。 出于此原因,多年前就定义了一个不同的编程模式:面向方面的编程 (AOP)。 顺便说一下,AOP 这一概念于 20 世纪 90 年代在 Xerox PARC 实验室中产生。 该团队还开发出第一种 AOP 语言(仍是最受欢迎的):AspectJ。
尽管几乎所有人都认同 AOP 的好处,但它仍未广泛实现。 在我看来,这种应用范围有限的主要原因基本上是缺乏合适的工具。 我深信,Microsoft .NET Framework 本机支持 AOP(即使只是部分支持)的那一天将成为 AOP 的历史转折点。 现在,您只能使用 ad hoc 框架在 .NET 中实现 AOP。
.NET 中 AOP 的最强大工具是 PostSharp,您可在 sharpcrafters.com 中找到。 PostSharp 提供一个完整的 AOP 框架,您可在该框架中体验 AOP 理论的所有关键功能。 但应注意,许多依赖关系注入 (DI) 框架都包括一些 AOP 功能。
例如,您会在 Spring.NET、Castle Windsor 当然还有 Microsoft Unity 中发现 AOP 功能。 对于相对简单的方案(例如,在应用层跟踪、缓存和修饰组件),DI 框架的功能通常能成功应用。 但对于域对象和 UI 对象,很难使用 DI 框架获得成功。 无疑,横切关注点会被视为外部依赖关系,而 DI 技术也必定允许您在类中注入外部依赖关系。
关键在于,DI 很可能将要求进行 ad hoc 前期设计或稍做重构。 换句话说,如果您已在使用 DI 框架,则很容易就能导入一些 AOP 功能。 反之,如果您的系统未使用 DI,则导入 DI 框架可能需要相当多的工作。 这在大型项目中或在更新旧系统的过程中并不总是可能实现的。 通过改用典型的 AOP 方法,可在一个称为“方面”的新组件中包装所有横切关注点。 在本文中,首先我将向您快速概述一下面向方面的模式,然后介绍您在 Unity 2.0 中发现的 AOP 相关功能。
AOP 快速指南
面向对象的编程 (OOP) 项目由多个源文件组成,每个源文件实现一个或多个类。 该项目还包括表示横切关注点(如日志记录或缓存)的类。 所有类均由编译器处理并生成可执行代码。 在 AOP 中,一个方面表示一个可重用的组件,它将多个类所需的行为封装在项目中。 实际处理方面的方式取决于您所考虑的 AOP 技术。 通常情况下,我们可以说各个方面并不简单直接地由编译器进行处理。 若要修改可执行代码以将方面考虑在内,需要一种额外的特定于技术的工具。 让我们大致看一下使用 AspectJ(第一个创建的 AOP 工具,即 Java AOP 编译器)会发生什么情况。
借助 AspectJ,您可使用 Java 编程语言来编写您的类,并使用 AspectJ 语言来编写方面。 AspectJ 支持自定义语法,您可通过自定义语法指示方面的预期行为。 例如,日志记录方面可能指定它将在调用特定方法之前和之后记录。 各个方面以某种方式合并到常规源代码中并产生源代码的中间版本,然后将该中间版本编译成可执行格式。 在 AspectJ 术语中,预处理方面并将方面与源代码合并的组件称为 weaver。 该组件产生一个编译器可呈现给可执行文件的输出。
总之,一个方面描述一段可重用的代码,您希望将可重用代码注入现有类中,而不接触这些类的源代码。 在其他 AOP 框架(如 .NET PostSharp 框架)中,您将找不到 weaver 工具。 但是,方面的内容始终由框架进行处理并生成某种形式的代码注入。
请注意,在这方面上,代码 注入不同于依赖关系 注入。 代码注入是指,AOP 框架能够将对方面中特定点处的公共终结点的调用插入到使用给定方面修饰的类主体中。 举例来说,PostSharp 框架让您能够将方面编写为 .NET 属性,然后将这些属性附加到类中的方法上。 PostSharp 属性由 PostSharp 编译器(我们甚至可以称之为 weaver)在生成后步骤中进行处理。 实际效果是,您的代码得到增强,从而在这些属性中包括一些代码。 但注入点将得到自动解析,您作为一名开发人员只需编写一个独立方面组件并将其附加到公共类方法即可。 代码易于编写,甚至更易于维护。
为了完成此次有关 AOP 的快速概述,我将介绍一些特定术语并解释它们各自的含义。 联接点 指示您要在目标类的源代码中注入方面代码的点。 pointcut 表示联接点集合。 建议 指的是要在目标类中注入的代码。 可在联接点的前后和四周注入代码。 一个建议与一个 pointcut 关联。 这些术语来自 AOP 的原始定义,在您使用的特定 AOP 框架中可能不会反映在字面上。 建议您尝试选取这些术语隐含的概念(AOP 的核心概念),然后使用这种知识更好地了解特定框架的详细信息。
Unity 2.0 快速指南
Unity 是作为 Microsoft Enterprise Library 项目的一部分提供的应用程序块,它也可单独下载。 Microsoft Enterprise Library 是应用程序块的集合,该集合处理大量描述 .NET 应用程序开发特征的横切关注点,如日志记录、缓存、加密、异常处理等。 Enterprise Library 的最新版本是 5.0,于 2010 年 4 月份发布,并附带对 Visual Studio 2010 的完全支持(在 msdn.microsoft.com/library/ff632023 上的“模式和实践开发人员中心”处,可了解该版本的详细信息)。
Unity 是 Enterprise Library 应用程序块之一。 Unity 同样适用于 Silverlight,它实质上是为拦截机制提供额外支持的 DI 容器,通过拦截机制可使您的类更加面向方面。
Unity 2.0 中的拦截功能
Unity 中拦截的核心理念是让开发人员能够自定义调用链,方便对对象调用方法。 也就是说,Unity 拦截机制通过在方法的常规执行前后或四周额外添加一些代码,捕获对已配置对象进行的调用并自定义目标对象的行为。 拦截实际上是在运行时向对象中添加新行为的一种极其灵活的方法,无需接触到对象的源代码,也不会影响相同继承路径中的类的行为。 Unity 拦截是实现 Decorator 模式的一种方式,该模式是一种常用设计模式,设计为在运行时扩展正在使用的对象的功能。 Decorator 是一个容器对象,它接收目标对象的实例(和维护对实例的引用),并向外界扩充其功能。
Unity 2.0 中的拦截机制同时支持实例拦截和类型拦截。 此外,不管实例化对象的方式如何,无论对象是通过 Unity 容器创建的还是一个已知实例,拦截都照常工作。 在后一种情况下,您只需使用一个不同的完全独立的 API 即可。 但是,如果您这么做,则将丢失配置文件支持。 图 1 演示 Unity 中拦截功能的体系结构,并详细说明该功能在未通过容器解析的特定对象实例上的工作方式。 (此图只是对 MSDN 文档中的某幅图稍做了一些修改。)
图 1 Unity 2.0 中对象拦截的工作方式
拦截子系统由三个关键元素组成:侦听器(或代理);行为管道;以及行为或方面。 这些子系统的两个极端分别为客户端应用程序和目标对象(即,被分配了未在其源代码中进行硬编码的其他行为的对象)。 在将客户端应用程序配置为在给定实例上使用 Unity 的拦截 API 后,所有方法调用都将通过一个代理对象(侦听器)。 此代理对象查看已注册行为的列表,并通过内部管道调用这些行为。 每个配置的行为都有机会在对象方法的常规调用之前或之后运行。 该代理将输入数据注入到管道中,然后在数据经目标对象最初生成接着由行为进一步修改后,该代理接收任何返回值。
配置拦截
在 Unity 2.0 中建议使用拦截的方法不同于早期版本,尽管在早期版本中使用的方法完全支持向后兼容性。 在 Unity 2.0 中,拦截只是您添加到容器中的一个新扩展,用来描述对象的实际解析方式。 下面是您希望通过 Fluent 代码配置拦截时所需的代码:
var container = new UnityContainer(); container.AddNewExtension<Interception>(); |
该容器需要查找有关要拦截的类型和要添加的行为的信息。 可使用 Fluent 代码或通过配置添加此信息。 我发现配置特别灵活,因为您无需接触应用程序也无需执行任何新的编译步骤,即可修改一些内容。 让我们采用基于配置的方法。
首先,在配置文件中添加以下内容:
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension. Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"/> |
此脚本的目的是使用特定于拦截子系统的新元素和别名来扩展配置架构。 另外,添加以下内容:
<container> <extension type="Interception" /> <register type="IBankAccount" mapTo="BankAccount"> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="TraceBehavior" /> </register> </container> |
若要使用 Fluent 代码实现相同的任务,您需要对容器对象调用 AddNewExtension<T> 和 RegisterType<T>。
让我们进一步看一下配置脚本。 <extension> 元素将拦截添加到容器中。 请注意,脚本中使用的“Interception”是在节扩展中定义的别名之一。 接口类型 IBankAccount 映射到具体类型 BankAccount(这是 DI 容器的典型作业),并与特定类型的侦听器相关联。 Unity 提供两种主要类型的侦听器:实例侦听器和类型侦听器。 下个月,我将深入探讨侦听器。 现在,一句话说明,实例侦听器创建一个代理来筛选针对已截获实例传入的调用。 相反,类型侦听器只是模拟已截获对象的类型,并在派生类型的实例上工作。 (有关侦听器的详细信息,请参阅 msdn.microsoft.com/library/ff660861(PandP.20)。)
接口侦听器是仅限于充当对象上一个接口的代理的实例侦听器。 接口侦听器使用动态代码生成来创建代理类。 配置中的拦截行为元素指示您要围绕已截获对象实例运行的外部代码。 必须通过声明的方式配置 TraceBehavior 类,以便容器可以解析该类及其任何依赖关系。 使用 <register> 元素告知容器 TraceBehavior 类及其所需的构造函数,如下所示:
图 2 显示 TraceBehavior 类中的一段摘录。
图 2 Unity 行为示例
class TraceBehavior : IInterceptionBehavior, IDisposable { private TraceSource source; public TraceBehavior(TraceSource source) { if (source == null) throw new ArgumentNullException("source"); this.source = source; } public IEnumerable<Type> GetRequiredInterfaces() { return Type.EmptyTypes; } public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { // BEFORE the target method execution this.source.TraceInformation("Invoking {0}", input.MethodBase.ToString()); // Yield to the next module in the pipeline var methodReturn = getNext().Invoke(input, getNext); // AFTER the target method execution if (methodReturn.Exception == null) { this.source.TraceInformation("Successfully finished {0}", input.MethodBase.ToString()); } else { this.source.TraceInformation( "Finished {0} with exception {1}: {2}", input.MethodBase.ToString(), methodReturn.Exception.GetType().Name, methodReturn.Exception.Message); } this.source.Flush(); return methodReturn; } public bool WillExecute { get { return true; } } public void Dispose() { this.source.Close(); } } |
行为类实现 IinterceptionBehavior,它基本上由 Invoke 方法组成。 Invoke 方法包含您要用于受侦听器控制的任何方法的整个逻辑。 如果您想要在调用目标方法之前执行一些操作,则在该方法开头执行操作。 当您想要运行到目标对象(或者更准确的说是运行到管道中注册的下一个行为)时,需调用框架提供的 getNext 委派。 最后,您可使用任何所需的代码对目标对象进行后处理。 Invoke 方法需要返回对管道中下一个元素的引用;如果返回 Null,则链中断,后续的行为将永远不会被调用。
配置灵活性
拦截(更笼统的说是 AOP)满足了许多有用的方案的要求。 例如,利用拦截,您可向各个对象中添加责任,而无需修改整个类,并且保持解决方案相对于使用 Decorator 来说更加灵活。
本文只涉及了应用于 .NET 的 AOP 的一些皮毛。 在接下来的几个月里,我将撰写有关 Unity 和 AOP 中拦截的更多内容。
关于作者
Dino Esposito 是 Microsoft Press (2010) 出版的《Programming Microsoft ASP.NET MVC》一书的作者,并且是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。Esposito 定居于意大利,经常在世界各地的业内活动中发表演讲。您可访问他的博客,网址为 weblogs.asp.net/despos。
原文链接:http://msdn.microsoft.com/zh-cn/magazine/gg490353.aspx