简介: 契约式设计(Design by Contract)是切实可行的技术,可以阐明组件设计的细节、为客户记录正确的组件用法,并用编程的方式测试组件使用的顺应性(compliance)。在 AOP@Work 的最后一篇中,Dean Wampler 介绍了 Contract4J,这是契约式设计的工具,它用 Java ™ 5 标注(annotation)指定合约,并在运行时用 AspectJ 方面计算合约。在成为 AOP 工具包中新增的一个举足轻重的工具的同时,Contract4J 迎合了面向方面设计的新趋势。
.关于本系列AOP@Work 系列面对的是在面向方面编程上有些基础并想扩展或加深了解的开发人员。与 developerWorks 的大多数文章一样,这个系列高度实用:读完每篇介绍新技术的文章,都可以立即投入使用。
被选择来撰写本系列文章的每个作者,都是面向方面编程的领头羊或专家。许多作者都是本系列中介绍的项目和工具的参与者。每篇文章都力图提供一个中立的评述,以确保这里表达的观点的公正与正确。
关于每篇文章的评论或问题,请分别与其作者联系。要对本系列整体进行评论,可以与本系列的负责人 Nicholas Lesiecki 联系。请参阅 参考资料,获取关于 AOP 的更多背景资料。
.假设您刚刚加入一个构建银行应用程序的项目。在浏览代码时,您发现(已经简化的)BankAccount 的下面这个接口:
interface BankAccount {
float getBalance();
float deposit(float amount);
float withdraw(float amount);
...
}
上面这个接口虽然简洁,但遗留了许多问题没有回答。deposit() 或 withdraw() 的 amount 参数可以是负数或零么?允许负余额(透支)么?如果指定了错误的 amount,deposit() 或 withdrawal() 中会发生什么情况呢?
显然,对于该接口的实现者和使用公开该接口的组件的人来说,能够回答这些问题是重要的。一种隐式地指定行为的方法是使用以 JUnit(请参阅 参考资料)编写的单元测试。使用 JUnit 测试,可以用各种合法和不合法的参数调用这些方法,并作出有关预期结果行为发生的断言。另一种方法是契约式设计,这是阐明组件设计细节的一项切实可行的技术。
在 AOP@Work 系列的最后这篇文章中,我将介绍 Contract4J,这是一个基于 AspectJ 的工具,支持契约式设计。我将介绍如何用 Contract4J 隐式地指定组件行为,为用户记录组件的正确用法,并用编程的方式测试组件使用的顺应性。在这篇文章最后,我将讨论 Contract4J 如何迎合面向方面设计中正在出现的趋势。
契约式设计概述
使用契约式设计,可以用可编程表达式指定对于组件输入和返回结果的要求。在开发人员和 QA 测试期间,对表达式进行计算,如果测试失败,程序执行立即终止。程序的终止带有有用的诊断信息,迫使开发人员立即修复 bug。
强制立即终止看起来可能有点麻烦。为什么要放过错误消息还继续运行呢?虽然继续运行看起来可能比较有生产效率,但实际上不是的。首先,如果没被强制要求立即处理 bug,就会推迟修复 bug,这样 bug 就会累积。其次,失败的测试应当代表发生了意料之外的事(例如,引用为空),正常的执行不能继续。虽然可以放入 “意外处理” 代码,但是这反而可能会把实现复杂化,出现永远不会发生的情况,从而增加代码的复杂性和更多 bug 的风险。
指定组件行为
契约式设计是一种发现和修复代码中逻辑错误的工具。它并不解决其他诸如性能、用户输入之类的问题。契约式设计使用三类测试来指定和确保组件的行为:
•对组件输入(例如传递给方法的参数)的测试叫做前置条件测试。它们指定组件执行请求的操作之前需要满足的条件。客户必须满足这些要求才能使用组件。
•后置条件测试 确保组件完成操作的时候结果符合要求,假设前置条件已经满足。后置条件测试通常用方法返回值的断言来表示。
•最后,不变条件测试 断言永远不变的条件。类在方法调用之前和之后必须保持不变(一旦对象已经构建)。方法在调用之前和之后必须保持不变;字段则在对象的整个生命周期中保持不变。
注意,在生产部署时可以关闭契约式设计测试,以消除它们的开销。
单元测试和契约式设计
契约式设计比起单元测试有些优势,但是这两种方法是互补的。契约式设计的一个主要优势是,它在接口或类本身当中提供了关于预期行为的显式信息。对于组件开发人员和客户来说,结果实际上就是一种能够用编程方式进行测试的文档。契约式设计也做了显式的合约定义,而在单元测试中这些更隐式。我喜欢交替使用这两种技术,特别是在使用难以进行单元测试的技术(例如 EJB2 bean)时。很快就会看到,Contract4J 添加了强迫使用约束的特性,这比 Junit 测试中隐式的非正式文档有相当大的优势。
--------------------------------------------------------------------------------
回页首
Contract4J 简介
Contract4J 是一个开源的开发人员工具,它用 Java 5 标注(请参阅 参考资料)实现契约式设计。在幕后,它用方面在应当执行测试的程序连接点处(例如,对方法的调用)插入 “建议”,它还对这些测试的失败进行处理,即终止程序执行。
再来看 BankAccount 接口,但是这次使用 Contract4J 标注。注意,我用黑体把原来的代码突出,并且对有些字符串作了换行,以便更加清晰:
清单 1. 使用 Contract4J 标注的 BankAccount
@Contract
@Invar("$this.balance > = 0.0")
interface BankAccount {
@Post("$return >= 0.0")
float getBalance();
@Pre("amount >= 0.0")
@Post("$this.balance == $old($this.balance)+amount
&& $return == $this.balance")
float deposit(float amount);
@Pre("amount >= 0.0 &&
$this.balance -- amount >= 0.0")
@Post("$this.balance == $old($this.balance)-amount
&& $return == $this.balance")
float withdraw(float amount);
...
}
表 1 定义了清单 1 中看到的关键字:
表 1. Contract4J 关键字示例
关键字 定义
$this 要测试的对象
$target 目前仅用于字段不变测试的字段(可以用 $this.field_name 来引用字段)。未来的使用可能会把 $target 扩展到其他上下文。
$return 方法返回的对象(或基本的值)。只在后置条件测试中有效。
$args[n] 传递给方法的第 n 个 参数,从 0 开始计数。也可以用名称引用参数。
$old 括号中的内容的 “旧” 值(在实际执行连接点之前)。只在不变条件测试和后置条件测试中有效。因为 Java 不要求所有的类都支持 “克隆”,所以 Contract4J 无法知道是否可克隆特定对象。所以,$old(...) 中的表达式应当只包含基本的值或不会改变的对象。否则 “旧” 值在连接点执行的时候可能会变化,从而产生意料之外的结果。示例表达式包含 $old("$this.userName") 和 $old("$this.calcMin(x,y)")。Contract4J 文档详细描述了允许的表达式。
BankAccount 合约
根据前一节学到的内容,清单 1 中的标注应当不再神秘。@Contract 标注表示拥有合约规范的接口(或类)。@Pre、@Post 和 @Invar 标注分别定义前置条件测试、后置条件测试和不变条件测试。还会注意到清单 1 中的测试是定义成字符串的 Java 表达式,结果为 true 或 false。
如果遗漏了测试表达式,Contract4J 会根据上下文使用合理的默认设置。例如,字段的默认不变条件要求字段不能为空。类似地,默认的方法前置条件要求所有非基本的输入参数不能为空,默认的方法后置条件要求返回值不能为空。
BankAccount 的接口规范包含一个类范围的不变条件测试,即余额总要大于或等于 0(对不起,不允许透支)。getBalance() 方法有一个后置条件,即它必须返回大于或等于 0 的值。注意,虽然接口不能定义字段,但不变条件测试和其他测试引用了一个隐含的余额字段。不过,因为接口定义了对应的 JavaBean 存取器方法,所以 Contract4J 推导出字段的存在。注意,getBalance() 的后置条件测试看起来可能与类的不变条件测试重复,但它只是部分地测试方法会返回余额这个假设。
合约还有几个前置条件,要求客户传递给 withdraw() 和 deposit() 的参数大于或等于 0。withdraw() 有额外的前置条件,要求取款的数额不能超过现有余额。最后,withdraw() 和 deposit() 有相似的后置条件要求;返回的值必须等于新余额,新余额必须等于旧余额减去或加上输入的数额。
通用提示
Contract4J 发行版中的 README(请参阅 下面)更详细地讨论了它的语法,包括已知的限制和特性。发行版中 Contract4J 自己的单元测试提供了有效和无效的测试表达式的丰富示例。
也可以在类或方面上编写合约测试,在这些类或方面中可以在构造函数上定义测试,在实例字段上定义不变条件。(上面的类不变条件实际上就是一个字段不变条件的规范!)方法和构造函数也可以有不变条件测试。
由于正如组件用户和子类所看到的,合约会影响离散的执行点,所以 Contract4J 把 public、protected 和包可见的方法当成 “原子的”。这意味着可以在方法中临时地违反测试,只要在方法完成的时候满足条件即可。注意,对于带有测试的其他方法或字段的调用也会触发这些测试,所以要有一些特殊情况的例外,以防止方面代码中的无限递归之类的问题。而且,Contract4J 目前不允许在 private 方法上定义测试,因为外部客户看不到这些方法。对静态方法的测试也不支持,因为它们不影响对象的状态。但是,在未来的版本中可能会消除这两个 “理论上的” 限制。
最后,字段的不变条件测试只是在读写字段之后才进行,以便允许惰性计算。类似地,字段的不变条件测试从不在对象构造期间进行,但是它们会在构造完成之后进行。
Contract4J 的替代
编写契约式设计测试,实际上可以不需要 Contract4J。只要编写自己的方面(就像在本系列前一篇文章中讨论的那样;请参阅 参考资料)即可。Contract4J 的优势是没有 AspectJ 经验的开发人员也能使用它;只需要对构建过程做简单的修改即可,下面我将讨论这个问题。Contract4J 还提供了非常简洁的定义测试的方式,采用熟悉的 Java 构造,不必定义许多额外的 AspectJ “样板文件”。合约不仅可以执行,还是用户可见的代码、文档和信息的组成部分,而如果是在分散的方面中捕获到的,这些信息则会很模糊。
正如前面提到过的,单元测试和契约式设计用不同的方式实现类似的目标。像 Contract4J 这样的契约式设计工具在单元测试比较分散或比较困难的时候最有帮助。集成和内置测试,有助于捕获这些经常被更低级的测试忽略的模糊的集成问题。不论是否使用 Contract4J 进行单元测试,考虑组件的合约都会改进设计。
--------------------------------------------------------------------------------
回页首
Contract4J
在运行时,Contract4J 使用内置的方面建议应该在其中执行测试的连接点。这些方面中的切入点查找合适的标注。前置条件测试由 before 建议处理,该建议就在对应的方法执行连接点之前执行。before 建议使用 Apache Jakarta Commons JEXL 解释器把测试字符串中的特殊关键字转换成合适的对象,并计算生成的表达式,返回 true(通过)或 false(失败)。如果测试失败,就报告错误消息,指出故障点,同时程序执行中断。
例如,在 清单 1 中,如果调用 withdraw(),那么就在执行方法之前,Contract4J 会用 amount 的输入值计算表达式 amount >= 0。例如,如果 amount = -1.0,那么测试失败,就会报告出带有堆栈信息、指出故障位置的报告,并且应用程序退出。
同样,后置条件测试大致与 after 建议对应。但是,为了支持 $old 关键字,实际上使用的是 around 建议,在该建议中,计算 $old 关键字中的 “子表达式”,保存结果,执行原来的连接点,然后插入 “旧” 值,再计算完整的测试表达式。
最后,不变条件测试使用 around 建议,在该建议中,在连接点执行之前和之后都计算测试,同时具有前面提到过的例外。
调用像 JEXL 这样的解析器确实会增加不小的开销,因为 Contract4J 只设计为在开发和测试期间使用,而在这两个期间内,开销不是严重的问题。但是,可能会发现有些频繁执行的代码块不应当拥有测试。
--------------------------------------------------------------------------------
回页首
采用 Contract4J
因为 Contract4J 的合约测试是用熟悉的 Java 规范编写的,所以把它采用到 Java 环境中很简单,包括四个步骤:
1.下载 Contract4J 并解压缩到方便的地方。除非想重新构建它(按照 README 中包含的说明),否则只需要 contract4j5.jar 文件。
2.把 contract4j5.jar 文件的位置添加到构建 CLASSPATH。
3.下载 并安装 AspectJ。
4.从当前 Java 编译器切换到 AspectJ 的 “ajc” 编译器,它也可以编译 Java 代码。AspectJ 的主页上提供了详细信息,发行版自带了 Ant 脚本。或者,如果喜欢继续使用现有的 Java 编译器,可以有两个附加选项:
◦可以在构建的末尾加入一个 ajc “织入” 步骤,把 contract4j5.jar 中的方面编织进预编译的类或 JAR 中。
◦可以在装入时 “织入” 合约,正如 AspectJ 文档中所解释的。
5.现在请开始把 Contract4J 标注添加到源代码中,以定义自己的合约!
定制 Contract4J
在运行时使用属性文件或 API 调用,可以开启或禁止所有测试,即前置条件测试、后置条件测试或者不变条件测试。正常情况下,对于生产部署,构建时应当不用 contract4j5.jar,以便不增加运行时开销。
使用 API 调用可以有丰富的定制,包括 “插件钩子”,用来插入自己的 Java 类,实现不同的行为。甚至可以替换 JEXL 表达式解释器。
Contract4J 的主页(请参阅 参考资料)提供了有关 API 、其他定制选项以及允许的测试表达式、已知的限制和特性方面的丰富文档。也可以在发行版中的构建 “ant docs” 目标,以生成完整的 Javadocs。
--------------------------------------------------------------------------------
回页首
Contract4J 和 AOP
除了是开发人员的有用工具之外,Contract4J 的意义还有两个原因:首先,它是越来越多的采用方面的 Java 开发工具中的一个,对于开发人员来说或多或少地是透明的。另一个示例是在本系列前面讨论过的 Glassbox Inspector。此外,Spring 框架大量地采用纯 Java 和 AspectJ 方面来支持中间件服务,而 JBoss 也使用纯 Java 方面实现同一目的。请参阅 参考资料,了解关于这三个项目的更多内容。
其次,Contract4J 使用简单的基于接口的方式进行方面设计。面向方面社区中的许多人目前都在把基于接口的设计这个概念从对象世界扩展到正在出现的方面/对象世界中,所以这个主题值得进一步讨论。
定义方面接口
标注通常用来指示代码的元信息。在这个示例中,Contract4J 用标注捕获组件的合约约束,这些约束已经成为了接口的一个基本组成部分,而不是 “附属于” 接口的东西。实际上,合约不是通常 AOP 意义上的 “横切”,不属于与组件的主要问题域 “正交” 的问题域的一部分。在 BankAccount 示例中,帐户余额允许的值,即帐户对象 “状态” 的一部分,是帐户对象的一个有机部分,而不是与帐户对象的正交。
所以,严格来说,契约式设计看起来可能根本不是 AOP 技术的备选方案。但是,虽然合约本身是 BankAccount 域所必不可少的部分,但这个信息的使用则是 横切的。Contract4J 的兴趣在于强制用编程的方式实现合约,而 Contract4J 标注的设计目的就是为了支持这个目标。不过,在自动生成单元测试的工具或 IDE 中,可以方便地利用通过标注公开的合约信息:如果组件使用不当,就会警告用户。
使用标注,Contract4J 定义了模式形式的协议,即一种接口,用来表达合约信息。在这一方面,Contract4J 类似于 Observer 模式:标注形式的合约规范是 “可以观察的”,可以由工具操作。协议用结构化英语的形式表达,例如方法后置条件可以这么表达:
if @Contract is on a class and
@Post is on a method, then
get the @Post expression string and
evaluate it, aborting if false.
Contract4J 方面用 AspectJ 实现这一逻辑。方面不要求被测试类的显式信息,例如它们的名称或方法名称。方面关心的只是标注。所以,方面可以完全保持通用和可重用。可以把这与许多典型的 AspectJ 方面对比,后者编写时显式地引用了特定的包和类,从而使重用和演变更加困难。
一个类似的使用标注来承载元信息的示例是,定义来与 Hibernate 和 EJB3 一起用于表达 POJO 中持久性需求的标注集(请参阅 参考资料)。
方面接口的挑战
当然,基于标注的元信息能走多远,依赖于方面设计技术能走多远。就像使用对象设计时,我们应当预料到创建可重用的、松散耦合的、面向方面的系统和可重用的、通用的方面库会大量地要求基于接口的技术。接口定义了耦合组件的适当抽象,却没有公开太多细节。所以,在软件发展的过程中,接口要比底层组件更稳定。
而就方面来说,它们带来了独特的挑战。从方面的性质来说,方面实际上广泛地接触到了系统的其余部分,而对象组件则更加 “本地化”。方面还有用新的、更精细的方式修改对象状态和行为的能力,从而引起了对维护系统完整性、健壮性、甚至整体理解的担心。
为了解决这些问题,研究人员和实践人员都在探寻方面/对象系统的接口不应只包含我们已经习惯的方法和状态信息,还应当包含合约信息,合约信息限定方面允许对其他组件所做的修改(听起来有点熟悉?)例如,方面可能不允许对组件做状态修改,或者出于性能和安全原因而被限制为不能建议某些 “关键区域”。
所以,不是方面切入点直接耦合到组件细节,比如特定的命名规范,方面而是要耦合到组件实现的接口。组件将用接口向方面公开允许的连接点和状态信息。方面将只通过公开的接口建议组件。由于接口要比实现它们的组件更稳定,所以耦合也会变得更稳定,在系统发展的时候,也更能跟得上变化。就像在对象系统中一样,基于方面的编程可以让设计人员能够更容易地构建健壮的、大型的、仍然可以重用的方面/对象系统。
--------------------------------------------------------------------------------
回页首
结束语
Contract4J 使用 Java 5 标注,以直观的方式使得契约式设计测试的定义变得更有效更简单。这些测试在测试时自动计算,有助于捕获代码中的逻辑错误。Contract4J 利用了 AspectJ 的威力,却不要求开发人员是使用 AspectJ 的专家。
参考资料
学习
•您可以参阅本文在 developerWorks 全球站点上的 英文原文。
•The Challenges of Writing Portable and Reusable Aspects in AspectJ: Lessons from Contract4J(Dean Wampler,AOSD 2006 年,ACM):作者把 Contract4J 作为编写真正可重用的通用方法的练习进行讨论。 请联系 Dean Wampler 获得拷贝。
•Modular Software Design with Crosscutting Interfaces(William G. Griswold,Macneil Shonle,Kevin Sullivan,Yuanyuan Song,Nishit Tewari,Yuanfang Cai,Hridesh Rajan;IEEE Software,2006 年 1 月/2 月):方面接口目前工作情况的一份精彩总结。
•“用 AOP 增强契约”(Filippo Diotalev,developerWorks,2004 年 7 月):介绍了如何手工编码契约式设计方面。
•“AOP@Work: 对方面进行单元测试”(Nicholas Lesiecki,developerWorks,2005 年 11 月):用于在 AspectJ 中测试横切行为的模式一览表。
•“AOP@Work: 用 AspectJ 进行性能监视,第 1 部分”(Ron Bodkin,developerWorks,2005 年 9 月):了解关于 Glassbox Inspector 的更多内容。
•AspectJ 主页:了解使用方面进行契约式设计测试的更多内容。
•Object-Oriented Software Construction, Second Edition(Bertrand Meyer,Prentice Hall,1997 年):包含发明者自己对契约式设计的介绍。
•AspectJ in Action(Ramnivas Laddad,Manning 2003 年):讨论了使用方面和观察者模式的策略实施。
•Java 技术专区:数百篇 Java 编程各方面的文章。
获得产品和技术
•Contract4J 主页:下载链接和使用 Contract4J 的更多细节。
•AspectJ 主页:下载最新的版本,获取关于 AspectJ 和 AJDT 的信息。
•JUnit:Java 平台最流行的单元测试框架。
•Barter:基于 XDoclet 的 Contract4J 的先驱,也实际采用了 AspectJ。
•JEXL 解释器:可以在 Jakarta 主页上找到它。
•Spring 和 JBoss:整合了方面的更加基于 Java 的技术。
讨论
•developerWorks blogs:加入 developerWorks 社区。
关于作者
Dean Wampler 通过他的公司 Aspect Research Associates 提供 AspectJ、企业级 Java 和 Ruby on Rails 的咨询。他是开源项目 Contract4J 的创始人。
.
转自:http://www.ibm.com/developerworks/cn/java/j-aopwork17.html
.关于本系列AOP@Work 系列面对的是在面向方面编程上有些基础并想扩展或加深了解的开发人员。与 developerWorks 的大多数文章一样,这个系列高度实用:读完每篇介绍新技术的文章,都可以立即投入使用。
被选择来撰写本系列文章的每个作者,都是面向方面编程的领头羊或专家。许多作者都是本系列中介绍的项目和工具的参与者。每篇文章都力图提供一个中立的评述,以确保这里表达的观点的公正与正确。
关于每篇文章的评论或问题,请分别与其作者联系。要对本系列整体进行评论,可以与本系列的负责人 Nicholas Lesiecki 联系。请参阅 参考资料,获取关于 AOP 的更多背景资料。
.假设您刚刚加入一个构建银行应用程序的项目。在浏览代码时,您发现(已经简化的)BankAccount 的下面这个接口:
interface BankAccount {
float getBalance();
float deposit(float amount);
float withdraw(float amount);
...
}
上面这个接口虽然简洁,但遗留了许多问题没有回答。deposit() 或 withdraw() 的 amount 参数可以是负数或零么?允许负余额(透支)么?如果指定了错误的 amount,deposit() 或 withdrawal() 中会发生什么情况呢?
显然,对于该接口的实现者和使用公开该接口的组件的人来说,能够回答这些问题是重要的。一种隐式地指定行为的方法是使用以 JUnit(请参阅 参考资料)编写的单元测试。使用 JUnit 测试,可以用各种合法和不合法的参数调用这些方法,并作出有关预期结果行为发生的断言。另一种方法是契约式设计,这是阐明组件设计细节的一项切实可行的技术。
在 AOP@Work 系列的最后这篇文章中,我将介绍 Contract4J,这是一个基于 AspectJ 的工具,支持契约式设计。我将介绍如何用 Contract4J 隐式地指定组件行为,为用户记录组件的正确用法,并用编程的方式测试组件使用的顺应性。在这篇文章最后,我将讨论 Contract4J 如何迎合面向方面设计中正在出现的趋势。
契约式设计概述
使用契约式设计,可以用可编程表达式指定对于组件输入和返回结果的要求。在开发人员和 QA 测试期间,对表达式进行计算,如果测试失败,程序执行立即终止。程序的终止带有有用的诊断信息,迫使开发人员立即修复 bug。
强制立即终止看起来可能有点麻烦。为什么要放过错误消息还继续运行呢?虽然继续运行看起来可能比较有生产效率,但实际上不是的。首先,如果没被强制要求立即处理 bug,就会推迟修复 bug,这样 bug 就会累积。其次,失败的测试应当代表发生了意料之外的事(例如,引用为空),正常的执行不能继续。虽然可以放入 “意外处理” 代码,但是这反而可能会把实现复杂化,出现永远不会发生的情况,从而增加代码的复杂性和更多 bug 的风险。
指定组件行为
契约式设计是一种发现和修复代码中逻辑错误的工具。它并不解决其他诸如性能、用户输入之类的问题。契约式设计使用三类测试来指定和确保组件的行为:
•对组件输入(例如传递给方法的参数)的测试叫做前置条件测试。它们指定组件执行请求的操作之前需要满足的条件。客户必须满足这些要求才能使用组件。
•后置条件测试 确保组件完成操作的时候结果符合要求,假设前置条件已经满足。后置条件测试通常用方法返回值的断言来表示。
•最后,不变条件测试 断言永远不变的条件。类在方法调用之前和之后必须保持不变(一旦对象已经构建)。方法在调用之前和之后必须保持不变;字段则在对象的整个生命周期中保持不变。
注意,在生产部署时可以关闭契约式设计测试,以消除它们的开销。
单元测试和契约式设计
契约式设计比起单元测试有些优势,但是这两种方法是互补的。契约式设计的一个主要优势是,它在接口或类本身当中提供了关于预期行为的显式信息。对于组件开发人员和客户来说,结果实际上就是一种能够用编程方式进行测试的文档。契约式设计也做了显式的合约定义,而在单元测试中这些更隐式。我喜欢交替使用这两种技术,特别是在使用难以进行单元测试的技术(例如 EJB2 bean)时。很快就会看到,Contract4J 添加了强迫使用约束的特性,这比 Junit 测试中隐式的非正式文档有相当大的优势。
--------------------------------------------------------------------------------
回页首
Contract4J 简介
Contract4J 是一个开源的开发人员工具,它用 Java 5 标注(请参阅 参考资料)实现契约式设计。在幕后,它用方面在应当执行测试的程序连接点处(例如,对方法的调用)插入 “建议”,它还对这些测试的失败进行处理,即终止程序执行。
再来看 BankAccount 接口,但是这次使用 Contract4J 标注。注意,我用黑体把原来的代码突出,并且对有些字符串作了换行,以便更加清晰:
清单 1. 使用 Contract4J 标注的 BankAccount
@Contract
@Invar("$this.balance > = 0.0")
interface BankAccount {
@Post("$return >= 0.0")
float getBalance();
@Pre("amount >= 0.0")
@Post("$this.balance == $old($this.balance)+amount
&& $return == $this.balance")
float deposit(float amount);
@Pre("amount >= 0.0 &&
$this.balance -- amount >= 0.0")
@Post("$this.balance == $old($this.balance)-amount
&& $return == $this.balance")
float withdraw(float amount);
...
}
表 1 定义了清单 1 中看到的关键字:
表 1. Contract4J 关键字示例
关键字 定义
$this 要测试的对象
$target 目前仅用于字段不变测试的字段(可以用 $this.field_name 来引用字段)。未来的使用可能会把 $target 扩展到其他上下文。
$return 方法返回的对象(或基本的值)。只在后置条件测试中有效。
$args[n] 传递给方法的第 n 个 参数,从 0 开始计数。也可以用名称引用参数。
$old 括号中的内容的 “旧” 值(在实际执行连接点之前)。只在不变条件测试和后置条件测试中有效。因为 Java 不要求所有的类都支持 “克隆”,所以 Contract4J 无法知道是否可克隆特定对象。所以,$old(...) 中的表达式应当只包含基本的值或不会改变的对象。否则 “旧” 值在连接点执行的时候可能会变化,从而产生意料之外的结果。示例表达式包含 $old("$this.userName") 和 $old("$this.calcMin(x,y)")。Contract4J 文档详细描述了允许的表达式。
BankAccount 合约
根据前一节学到的内容,清单 1 中的标注应当不再神秘。@Contract 标注表示拥有合约规范的接口(或类)。@Pre、@Post 和 @Invar 标注分别定义前置条件测试、后置条件测试和不变条件测试。还会注意到清单 1 中的测试是定义成字符串的 Java 表达式,结果为 true 或 false。
如果遗漏了测试表达式,Contract4J 会根据上下文使用合理的默认设置。例如,字段的默认不变条件要求字段不能为空。类似地,默认的方法前置条件要求所有非基本的输入参数不能为空,默认的方法后置条件要求返回值不能为空。
BankAccount 的接口规范包含一个类范围的不变条件测试,即余额总要大于或等于 0(对不起,不允许透支)。getBalance() 方法有一个后置条件,即它必须返回大于或等于 0 的值。注意,虽然接口不能定义字段,但不变条件测试和其他测试引用了一个隐含的余额字段。不过,因为接口定义了对应的 JavaBean 存取器方法,所以 Contract4J 推导出字段的存在。注意,getBalance() 的后置条件测试看起来可能与类的不变条件测试重复,但它只是部分地测试方法会返回余额这个假设。
合约还有几个前置条件,要求客户传递给 withdraw() 和 deposit() 的参数大于或等于 0。withdraw() 有额外的前置条件,要求取款的数额不能超过现有余额。最后,withdraw() 和 deposit() 有相似的后置条件要求;返回的值必须等于新余额,新余额必须等于旧余额减去或加上输入的数额。
通用提示
Contract4J 发行版中的 README(请参阅 下面)更详细地讨论了它的语法,包括已知的限制和特性。发行版中 Contract4J 自己的单元测试提供了有效和无效的测试表达式的丰富示例。
也可以在类或方面上编写合约测试,在这些类或方面中可以在构造函数上定义测试,在实例字段上定义不变条件。(上面的类不变条件实际上就是一个字段不变条件的规范!)方法和构造函数也可以有不变条件测试。
由于正如组件用户和子类所看到的,合约会影响离散的执行点,所以 Contract4J 把 public、protected 和包可见的方法当成 “原子的”。这意味着可以在方法中临时地违反测试,只要在方法完成的时候满足条件即可。注意,对于带有测试的其他方法或字段的调用也会触发这些测试,所以要有一些特殊情况的例外,以防止方面代码中的无限递归之类的问题。而且,Contract4J 目前不允许在 private 方法上定义测试,因为外部客户看不到这些方法。对静态方法的测试也不支持,因为它们不影响对象的状态。但是,在未来的版本中可能会消除这两个 “理论上的” 限制。
最后,字段的不变条件测试只是在读写字段之后才进行,以便允许惰性计算。类似地,字段的不变条件测试从不在对象构造期间进行,但是它们会在构造完成之后进行。
Contract4J 的替代
编写契约式设计测试,实际上可以不需要 Contract4J。只要编写自己的方面(就像在本系列前一篇文章中讨论的那样;请参阅 参考资料)即可。Contract4J 的优势是没有 AspectJ 经验的开发人员也能使用它;只需要对构建过程做简单的修改即可,下面我将讨论这个问题。Contract4J 还提供了非常简洁的定义测试的方式,采用熟悉的 Java 构造,不必定义许多额外的 AspectJ “样板文件”。合约不仅可以执行,还是用户可见的代码、文档和信息的组成部分,而如果是在分散的方面中捕获到的,这些信息则会很模糊。
正如前面提到过的,单元测试和契约式设计用不同的方式实现类似的目标。像 Contract4J 这样的契约式设计工具在单元测试比较分散或比较困难的时候最有帮助。集成和内置测试,有助于捕获这些经常被更低级的测试忽略的模糊的集成问题。不论是否使用 Contract4J 进行单元测试,考虑组件的合约都会改进设计。
--------------------------------------------------------------------------------
回页首
Contract4J
在运行时,Contract4J 使用内置的方面建议应该在其中执行测试的连接点。这些方面中的切入点查找合适的标注。前置条件测试由 before 建议处理,该建议就在对应的方法执行连接点之前执行。before 建议使用 Apache Jakarta Commons JEXL 解释器把测试字符串中的特殊关键字转换成合适的对象,并计算生成的表达式,返回 true(通过)或 false(失败)。如果测试失败,就报告错误消息,指出故障点,同时程序执行中断。
例如,在 清单 1 中,如果调用 withdraw(),那么就在执行方法之前,Contract4J 会用 amount 的输入值计算表达式 amount >= 0。例如,如果 amount = -1.0,那么测试失败,就会报告出带有堆栈信息、指出故障位置的报告,并且应用程序退出。
同样,后置条件测试大致与 after 建议对应。但是,为了支持 $old 关键字,实际上使用的是 around 建议,在该建议中,计算 $old 关键字中的 “子表达式”,保存结果,执行原来的连接点,然后插入 “旧” 值,再计算完整的测试表达式。
最后,不变条件测试使用 around 建议,在该建议中,在连接点执行之前和之后都计算测试,同时具有前面提到过的例外。
调用像 JEXL 这样的解析器确实会增加不小的开销,因为 Contract4J 只设计为在开发和测试期间使用,而在这两个期间内,开销不是严重的问题。但是,可能会发现有些频繁执行的代码块不应当拥有测试。
--------------------------------------------------------------------------------
回页首
采用 Contract4J
因为 Contract4J 的合约测试是用熟悉的 Java 规范编写的,所以把它采用到 Java 环境中很简单,包括四个步骤:
1.下载 Contract4J 并解压缩到方便的地方。除非想重新构建它(按照 README 中包含的说明),否则只需要 contract4j5.jar 文件。
2.把 contract4j5.jar 文件的位置添加到构建 CLASSPATH。
3.下载 并安装 AspectJ。
4.从当前 Java 编译器切换到 AspectJ 的 “ajc” 编译器,它也可以编译 Java 代码。AspectJ 的主页上提供了详细信息,发行版自带了 Ant 脚本。或者,如果喜欢继续使用现有的 Java 编译器,可以有两个附加选项:
◦可以在构建的末尾加入一个 ajc “织入” 步骤,把 contract4j5.jar 中的方面编织进预编译的类或 JAR 中。
◦可以在装入时 “织入” 合约,正如 AspectJ 文档中所解释的。
5.现在请开始把 Contract4J 标注添加到源代码中,以定义自己的合约!
定制 Contract4J
在运行时使用属性文件或 API 调用,可以开启或禁止所有测试,即前置条件测试、后置条件测试或者不变条件测试。正常情况下,对于生产部署,构建时应当不用 contract4j5.jar,以便不增加运行时开销。
使用 API 调用可以有丰富的定制,包括 “插件钩子”,用来插入自己的 Java 类,实现不同的行为。甚至可以替换 JEXL 表达式解释器。
Contract4J 的主页(请参阅 参考资料)提供了有关 API 、其他定制选项以及允许的测试表达式、已知的限制和特性方面的丰富文档。也可以在发行版中的构建 “ant docs” 目标,以生成完整的 Javadocs。
--------------------------------------------------------------------------------
回页首
Contract4J 和 AOP
除了是开发人员的有用工具之外,Contract4J 的意义还有两个原因:首先,它是越来越多的采用方面的 Java 开发工具中的一个,对于开发人员来说或多或少地是透明的。另一个示例是在本系列前面讨论过的 Glassbox Inspector。此外,Spring 框架大量地采用纯 Java 和 AspectJ 方面来支持中间件服务,而 JBoss 也使用纯 Java 方面实现同一目的。请参阅 参考资料,了解关于这三个项目的更多内容。
其次,Contract4J 使用简单的基于接口的方式进行方面设计。面向方面社区中的许多人目前都在把基于接口的设计这个概念从对象世界扩展到正在出现的方面/对象世界中,所以这个主题值得进一步讨论。
定义方面接口
标注通常用来指示代码的元信息。在这个示例中,Contract4J 用标注捕获组件的合约约束,这些约束已经成为了接口的一个基本组成部分,而不是 “附属于” 接口的东西。实际上,合约不是通常 AOP 意义上的 “横切”,不属于与组件的主要问题域 “正交” 的问题域的一部分。在 BankAccount 示例中,帐户余额允许的值,即帐户对象 “状态” 的一部分,是帐户对象的一个有机部分,而不是与帐户对象的正交。
所以,严格来说,契约式设计看起来可能根本不是 AOP 技术的备选方案。但是,虽然合约本身是 BankAccount 域所必不可少的部分,但这个信息的使用则是 横切的。Contract4J 的兴趣在于强制用编程的方式实现合约,而 Contract4J 标注的设计目的就是为了支持这个目标。不过,在自动生成单元测试的工具或 IDE 中,可以方便地利用通过标注公开的合约信息:如果组件使用不当,就会警告用户。
使用标注,Contract4J 定义了模式形式的协议,即一种接口,用来表达合约信息。在这一方面,Contract4J 类似于 Observer 模式:标注形式的合约规范是 “可以观察的”,可以由工具操作。协议用结构化英语的形式表达,例如方法后置条件可以这么表达:
if @Contract is on a class and
@Post is on a method, then
get the @Post expression string and
evaluate it, aborting if false.
Contract4J 方面用 AspectJ 实现这一逻辑。方面不要求被测试类的显式信息,例如它们的名称或方法名称。方面关心的只是标注。所以,方面可以完全保持通用和可重用。可以把这与许多典型的 AspectJ 方面对比,后者编写时显式地引用了特定的包和类,从而使重用和演变更加困难。
一个类似的使用标注来承载元信息的示例是,定义来与 Hibernate 和 EJB3 一起用于表达 POJO 中持久性需求的标注集(请参阅 参考资料)。
方面接口的挑战
当然,基于标注的元信息能走多远,依赖于方面设计技术能走多远。就像使用对象设计时,我们应当预料到创建可重用的、松散耦合的、面向方面的系统和可重用的、通用的方面库会大量地要求基于接口的技术。接口定义了耦合组件的适当抽象,却没有公开太多细节。所以,在软件发展的过程中,接口要比底层组件更稳定。
而就方面来说,它们带来了独特的挑战。从方面的性质来说,方面实际上广泛地接触到了系统的其余部分,而对象组件则更加 “本地化”。方面还有用新的、更精细的方式修改对象状态和行为的能力,从而引起了对维护系统完整性、健壮性、甚至整体理解的担心。
为了解决这些问题,研究人员和实践人员都在探寻方面/对象系统的接口不应只包含我们已经习惯的方法和状态信息,还应当包含合约信息,合约信息限定方面允许对其他组件所做的修改(听起来有点熟悉?)例如,方面可能不允许对组件做状态修改,或者出于性能和安全原因而被限制为不能建议某些 “关键区域”。
所以,不是方面切入点直接耦合到组件细节,比如特定的命名规范,方面而是要耦合到组件实现的接口。组件将用接口向方面公开允许的连接点和状态信息。方面将只通过公开的接口建议组件。由于接口要比实现它们的组件更稳定,所以耦合也会变得更稳定,在系统发展的时候,也更能跟得上变化。就像在对象系统中一样,基于方面的编程可以让设计人员能够更容易地构建健壮的、大型的、仍然可以重用的方面/对象系统。
--------------------------------------------------------------------------------
回页首
结束语
Contract4J 使用 Java 5 标注,以直观的方式使得契约式设计测试的定义变得更有效更简单。这些测试在测试时自动计算,有助于捕获代码中的逻辑错误。Contract4J 利用了 AspectJ 的威力,却不要求开发人员是使用 AspectJ 的专家。
参考资料
学习
•您可以参阅本文在 developerWorks 全球站点上的 英文原文。
•The Challenges of Writing Portable and Reusable Aspects in AspectJ: Lessons from Contract4J(Dean Wampler,AOSD 2006 年,ACM):作者把 Contract4J 作为编写真正可重用的通用方法的练习进行讨论。 请联系 Dean Wampler 获得拷贝。
•Modular Software Design with Crosscutting Interfaces(William G. Griswold,Macneil Shonle,Kevin Sullivan,Yuanyuan Song,Nishit Tewari,Yuanfang Cai,Hridesh Rajan;IEEE Software,2006 年 1 月/2 月):方面接口目前工作情况的一份精彩总结。
•“用 AOP 增强契约”(Filippo Diotalev,developerWorks,2004 年 7 月):介绍了如何手工编码契约式设计方面。
•“AOP@Work: 对方面进行单元测试”(Nicholas Lesiecki,developerWorks,2005 年 11 月):用于在 AspectJ 中测试横切行为的模式一览表。
•“AOP@Work: 用 AspectJ 进行性能监视,第 1 部分”(Ron Bodkin,developerWorks,2005 年 9 月):了解关于 Glassbox Inspector 的更多内容。
•AspectJ 主页:了解使用方面进行契约式设计测试的更多内容。
•Object-Oriented Software Construction, Second Edition(Bertrand Meyer,Prentice Hall,1997 年):包含发明者自己对契约式设计的介绍。
•AspectJ in Action(Ramnivas Laddad,Manning 2003 年):讨论了使用方面和观察者模式的策略实施。
•Java 技术专区:数百篇 Java 编程各方面的文章。
获得产品和技术
•Contract4J 主页:下载链接和使用 Contract4J 的更多细节。
•AspectJ 主页:下载最新的版本,获取关于 AspectJ 和 AJDT 的信息。
•JUnit:Java 平台最流行的单元测试框架。
•Barter:基于 XDoclet 的 Contract4J 的先驱,也实际采用了 AspectJ。
•JEXL 解释器:可以在 Jakarta 主页上找到它。
•Spring 和 JBoss:整合了方面的更加基于 Java 的技术。
讨论
•developerWorks blogs:加入 developerWorks 社区。
关于作者
Dean Wampler 通过他的公司 Aspect Research Associates 提供 AspectJ、企业级 Java 和 Ruby on Rails 的咨询。他是开源项目 Contract4J 的创始人。
.
转自:http://www.ibm.com/developerworks/cn/java/j-aopwork17.html