用AOP增强契约:用AspectJ为Java软件开发加入契约式设计

简介:在开发企业软件时,Java 代码经常需要与外部组件交互。不管应用程 序必须与遗留应用程序、外部系统还是第三方库通信,使用不能控制的组件会引 入非预期结果的风险。IBM 的 IT 专家 Filippo Diotalevi 展示了,面向方面 的 编程 (AOP) 如何通过帮助您在保持代码的干净和灵活性的同时,设计和定义 组 件之间的明确契约,从而降低这种风险。

契约式设计(Design by Contract)(DBC) 是面向对象的软件设计中的一 种 技术,它的目的是保证软件质量、可靠性和可重用性。DBC 中的关键考虑是可以 通过以下做法实现这个目标:

尽可能准确地规定组件之间的通信。

定义通信过程中的相互责任和预期的结果。

这些相互责任称为 契约,用 断言检查应用程序是否满足契约。简单地说, 断 言是插入到程序执行中的特定点的布尔表达式,它必须为真。失败的断言通常是 软件 bug 的症兆,所以必须将它报告给使用者。

在处理外部组件或者库,并需要保证应用程序传递给它们的数据和从它们那 里 接收的数据是正确的时候,DBC 特别有用。本文将展示一个抽象的基础设施和一 个示例应用程序,前者使用面向方面的编程(AOP)实现 DBC,后者与外部组件 建 立契约。

断言和 Java 语言

DBC 识别三种基本的断言类型:

前置条件: 客户为了正确调用外部组件而必须满足的责任。

后置条件: 执行外部组件后的预期结果。

不变量: 在执行了外部组件后维持不变的条件。

Java 语言原来没有提供对断言的天然支持。 assert 语句是在版本 1.4 中 加 入的。不过,在日常编码中使用 DBC 会是一种挑战。事实上,大多数常用的方 法 ── 在应用程序代码中直接加入前置和后置断言 ── 在代码模块化和可重用 性 方面有严重的缺点。这种方法是 纠缠的代码 的一个活生生的例子:它混合了业 务逻辑代码与断言所需的非功能代码。这种代码是不灵活的,因为不能在不改变 应用程序代码的情况下改变或者删除断言。

对这个问题的理想解决方案要满足四个要求:

透明性: 前置和后置条件代码不与业务逻辑混合。

可重用性: 解决方案的大多数部件是可重用的。

灵活性: 可以用简单的方式增加、删除和修改断言模块。

简单性: 可以用简单的语法指定断言。

用 AOP 进行透明的契约式设计

如果目的是分离关注点、透明性和灵活性,那么面向方面的编程 (AOP) 通常 就是正确的答案。前置条件、后置条件和不变量是 横切关注点 (crosscutting concern)── 常用于应用程序的各种模块中常常包含的功能中,在某种程度上 与应用程序代码相混合。AOP 的目标是让开发人员可以在单独的模块中编写这些 功能并以灵活和声明式的方式应用它们。

本文假定您对 AspectJ 中的 AOP 有一般性的了解,并且不准备介绍 AOP。 有 关这个主题的介绍文章清单请参阅 参考资料。

实现基础设施

满足我在前面列出的四项条件的解决方案包含三部分,如图 1 所示:

应用程序代码(不包含与 DBC 有关的元素)。

契约实现(有前置条件、后置条件和不变量检查)。

一个作为代码与契约之间 桥梁的对象,可以将契约应用到代码中正确的部分 中并有正确的逻辑。

图 1. 契约式设计的一个模块化得很好的解决方案

图 1 所展示的设计保证了高度灵活的解决方案,它使您可以不用改变契约实 现或者应用程序代码就可以应用或者删除契约。并且它对于应用程序是完全透明 的。

实现细节

契约是实现了特定接口的 Java 类,“桥梁” 是 AspectJ 方面。这个方面 指 定了应用契约的特定点和应用契约所需要的逻辑,如图 2 所示。

图 2. 以操作图表示的契约式设计逻辑

图 2 中的逻辑图对于所有契约都是相同的,所以可以开发一个指定它的公共 抽象方面。图 3 显示了这个解决方案的类和方面图:

图 3. 契约检查器系统的基本组件

AbstractContract 方面指定在 图 2 中的操作图中声明的控制逻辑。它在程 序的执行中留下了未表示的(即抽象的)点,(由 ContractManager 接口的实 现 定义的)契约将应用到这里。 ConcreteContract 方面(扩展 AbstractContract 方面) 负责:

通过 targetPointcut pointcut 指定进行契约检查的准确位置。

通过 getContractManager() 方法指定负责检查契约的类。

包含检查应用程序与外部模块之间契约的代码的类是 ContractManager 接口 的一个实现。清单 1 所示的 ContractManager 是一个定义契约检查类的基本行 为的简单 Java 接口:

清单 1. ContractManager 接口

public interface ContractManager{  /**  * Check the preconditions  */  public void checkPreConditions(Object thisObject, Object[] args)   throws ContractBrokeException;  /**  * Check the postconditions  */      public void checkPostConditions(Object thisObject, Object returnValue, Object [] args)   throws ContractBrokeException;  /**  * Check the invariants  */  public void checkInvariants(Object thisObject) throws ContractBrokeException;}

ContractManager 接口为要检查的每一种断言定义了不同的方法。每一个方 法 可以通过 thisObject 参数访问 Java 对象,该对象调用要保证其契约的函数。 前置条件和后置条件方法可以看到作为函数参数 ( args ) 传递的值。只有后 置条件方法可以通过 returnValue 参数接收最终的返回值。通过结合使用这三 种 方法,可以检查几乎所有常见的条件。

AbstractContract 方面执行进行契约检查所需要的控制逻辑。这个逻辑是在 around():targetPointcut() advice 中表述的。清单 2 显示了 AbstractContract 方面:

清单 2. AbstractContract 方面

public abstract aspect AbstractContract{  /**  * Define the pointcut to apply the contract checking  * MUST CONTAIN A METHOD CALL  */     public abstract pointcut targetPointcut();  /**  * Define the ContractManager interface implemenTor to be used  */     public abstract ContractManager getContractManager();  /**  * Perform. the logic necessary to perform. contract checking  */      Object around(): targetPointcut()  {    ContractManager cManager = getContractManager();    System.out.println("Checking contract using:" + cManager.getClass ().getName());   if (cManager!=null)   {     System.out.println("Performing initial invariants check");     cManager.checkInvariants(thisJoinPoint.getTarget());   }     if (cManager!=null)   {     System.out.println ("Performing pre-conditions check");     cManager.checkPreConditions(thisJoinPoint.getTarget(), thisJoinPoint.getArgs());   }   Object bj = proceed ();   if (cManager!=null)   {     System.out.println("Performing post conditions check");     cManager.checkPostConditions(thisJoinPoint.getTarget(), obj, thisJoinPoint.getArgs());   }   if (cManager!=null)    {     System.out.println("Performing final invariants check");     cManager.checkInvariants(thisJoinPoint.getTarget ());   }   return obj;  }}

AbstractContract 方面表示两个抽象方法,在实现具体的契约检查器方面时 必须实现这两个方法:

public abstract pointcut targetPointcut() 表示其中必须应用 advice 的 pointcut。pointcut 必须是一个方法调用。

public abstract ContractManager getContractManager() 必须返回实现了 正确的契约检查的 ContractManager 的一个实例。

一定要注意不变量检查执行了两次,是在服务执行之前和之后。这使您可以 检 查服务的执行有没有影响一些外部字段的值。

契约的失败会导致 ContractBrokeException ,这会停止 advice 的执行。

实际的契约检查

理解了用 AOP 实现契约式设计的必要基础设施后,就可以让它工作了。假定 需要查询一个外部客户关系管理 (CRM) 系统以获取客户的数据。可能像下面 这 样调用 CRM 系统:

Customer cus = companyCustomerSystem.getCustomer(“Pluto”);

从开发人员的角度看, getCustomer 函数的实现是不重要的,因为 getCustomer 是一个外部组件。但是检查它是否返回破坏性的结果是非常重要的 。它与保证应用程序不传递错误或者无意义的输入给 CRM 系统同样重要。可以 通 过开发一个扩展了 AbstractContract 的具体方面解决这两种意外情况。具体的 方面覆盖两个方法:

targetPointcut(),定义应用契约检查的 pointcut。

getContractManager(),定义负责执行所有检查的 ContractManager实现。

清单 3 显示了示例应用程序的具体方面:

清单 3. 具体契约方面

public aspect CcCompanySystem extends AbstractContract{public pointcut targetPointcut(): call(Customer CompanySystem.getCustomer(String));public ContractManager getContractManager(){return new CompanySystemContractManager();}}

CcCompanySystem方面指定契约检查器调用的 CompanySystemContractManager将对由 CompanySystem类的 getCustomer方法的 调用所表示的 pointcut 应用。不需要定义契约检查操作的控制逻辑,因为它继 承自 清单 2中的前辈 AbstractContract抽象方面。

最后一步是开发一个进行契约检查的 Java 类。如前所述,这个类必须实现 ContractManager接口。清单 4 显示了一个示例 CompanySystemContractManager类:

清单 4. 示例应用程序的 ContractManager 实现

public class CompanySystemContractManager implements ContractManager{/*** Check preconditions*/public void checkPreConditions(Object thisObject, Object[] args)throws ContractBrokeException{Object arg = args[0];if (arg == null){throw new ContractBrokeException("PRECONDITION ERROR: " +" Argument of getCustomer shouldn't be null");}}/*** Check postconditions*/public void checkPostConditions(Object thisObject, Object value, Object[] args)throws ContractBrokeException{if (value == null){throw new ContractBrokeException("POSTCONDITION ERROR: " +" Return value of getCustomer shouldn't be null");}}/*** Check invariants*/public void checkInvariants(Object thisObject) throws ContractBrokeException{//invariants check}}

清单 4 中的 CompanySystemContractManager类只检查参数或者返回值是否 为 nulll,但是可以使其增强为加入特别复杂的检查。

要注意的重要一点:每一个契约检查实例化一个 CompanySystemContractManager对象,因此可以通过在第一个不变量检查期间将 数据存储到私有字段中,并在执行完 CRM 系统调用后验证它们有没有改变,来 检查不变量。

恭喜!您已经开发了应用程序与 CRM 系统之间的一个简单的契约。在用 AspectJ 编译器编译这个应用程序后,这个契约将应用到对 CompanySystem类的 getCustomer方法的每一次调用上,并检查应用程序与它之间的交互的一致性。 而且,如果 CompanySystemContractManager足够一般化,就可以重复使用它, 只需要重新定义 targetPointcut就可以将它用于其他的契约检查。

这个示例解决方案完全满足我在本文开始时列出的四项要求:

它是 透明的,因为业务逻辑代码不包含对契约检查的引用,前者绝对不知道 后者。

它是 可重用的,因为它依赖于一个简单的基础设(一个接口和一个抽象方面 ),并使您可以在多种情况下重复使用一个 ContractManager。

它是 灵活的,因为可以使用 AspectJ 编译器帮助选择使用哪些方面,从而 选择要检查哪些契约。

它是 简单的,因为它只由几个类组成。

结束语

本文描述了在使用 AspectJ 和 AOP 进行 Java 应用程序开发时采用契约式 设计的可能方式。建议的解决方案保证了干净而灵活的解决方案,因为它使用一 个特别简单且很好地模块化的设计,使您可以将契约与业务逻辑分开编写并声明 式地应用它们。

本文配套源码

接受失败等于放松自己高压的心理,

用AOP增强契约:用AspectJ为Java软件开发加入契约式设计

相关文章:

你感兴趣的文章:

标签云: