级别: 初级 Filippo Diotalevi (filippo.diotalevi@it.ibm.com), IT 专家, IBM Italy
2004 年 7 月 15 日 在开发企业软件时,Java 代码经常需要与外部组件交互。不管应用程序必须与遗留应用程序、外部系统还是第三方库通信,使用不能控制的组件会引入非预期结果的风险。IBM 的 IT 专家 Filippo Diotalevi 展示了,面向方面的编程 (AOP) 如何通过帮助您在保持代码的干净和灵活性的同时,设计和定义组件之间的明确契约,从而降低这种风险。
契约式设计(Design by Contract)(DBC) 是面向对象的软件设计中的一种技术,它的目的是保证软件质量、可靠性和可重用性。DBC 中的关键考虑是可以通过以下做法实现这个目标:
- 尽可能准确地规定组件之间的通信。
- 定义通信过程中的相互责任和预期的结果。
这些相互责任称为
契约,用
断言检查应用程序是否满足契约。简单地说,断言是插入到程序执行中的特定点的布尔表达式,它必须为真。失败的断言通常是软件 bug 的症兆,所以必须将它报告给使用者。
在处理外部组件或者库,并需要保证应用程序传递给它们的数据和从它们那里接收的数据是正确的时候,DBC 特别有用。本文将展示一个抽象的基础设施和一个示例应用程序,前者使用面向方面的编程(AOP)实现 DBC,后者与外部组件建立契约。
断言和 Java 语言
DBC 识别三种基本的断言类型:
-
前置条件: 客户为了正确调用外部组件而必须满足的责任。
-
后置条件: 执行外部组件后的预期结果。
-
不变量: 在执行了外部组件后维持不变的条件。
Java 语言原来没有提供对断言的天然支持。
assert 语句是在版本 1.4 中加入的。不过,在日常编码中使用 DBC 会是一种挑战。事实上,大多数常用的方法 ── 在应用程序代码中直接加入前置和后置断言 ── 在代码模块化和可重用性方面有严重的缺点。这种方法是
纠缠的代码 的一个活生生的例子:它混合了业务逻辑代码与断言所需的非功能代码。这种代码是不灵活的,因为不能在不改变应用程序代码的情况下改变或者删除断言。
 |
DBC 斗士
Bertrand Meyer 的
Object Oriented Software Construction一书(请参阅
参考资料) 描述和定形了契约式设计。Meyer 呼吁采用 DBC 作为编写健壮和可靠的面向对象的代码的主要和系统的方式。他发明了 Eiffel 编程语言,该语言很好地支持断言。“Design by Contract”是 Interactive Software Engineering 的商标,这是 Meyer 为开发和发布 Eiffel 而合伙创建的公司。
|
|
对这个问题的理想解决方案要满足四个要求:
-
透明性: 前置和后置条件代码不与业务逻辑混合。
-
可重用性: 解决方案的大多数部件是可重用的。
-
灵活性: 可以用简单的方式增加、删除和修改断言模块。
-
简单性: 可以用简单的语法指定断言。
用 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 obj = 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 应用程序开发时采用契约式设计的可能方式。建议的解决方案保证了干净而灵活的解决方案,因为它使用一个特别简单且很好地模块化的设计,使您可以将契约与业务逻辑分开编写并声明式地应用它们。
下载 | 名字 | 大小 | 下载方法 |
|---|
| j-ceaop-source.zip | | HTTP |
参考资料
关于作者
对本文的评价
|