很自然,我也会编写一个快速测试用例来验证我的检验是否真能避免 NullPointerException,如清单 4 所示:
清单 4. 验证 null 检验
@Test(expectedExceptions={RuntimeException.class})
public void verifyHierarchyNull() throws Exception{
Class clzz = null;
HierarchyBuilder.buildHierarchy(null);
}
在本例中,防御性编程似乎解决了问题。但仅依靠这项策略会存在一些缺陷。
防御的缺陷
关于断言
清单 3 使用一个条件来验证 clzz 的值,实际上 assert 也同样好用。使用断言,无需指定条件,也不需要指定异常语句。在启用了断言的情况下,防御性编程的关注点全部由 JVM 处理。
尽管防御性编程有效地保证了方法的输入条件,但如果在一系列方法中使用它,不免过于重复。熟悉面向方面编程(或 AOP)的人们会把它认为是横切关注点,这意味着防御性编程技术横跨了代码库。许多不同的对象都采用这些语法,尽管从纯面向对象的观点来看这些语法跟对象毫不相关。
而且,横切关注点开始渗入到契约式设计(DBC)的概念中。DBC 是这样一项技术,它通过在组件的接口显式地陈述每个组件应有的功能和客户机的期望值来确保系统中所有的组件完成它们应尽的职责。从 DBC 的角度讲,组件应有的功能被认为是后置条件,本质上是组件的责任,而客户机的期望值则普遍被认为是前置条件。另外,在纯 DBC 术语中,遵循 DBC 规则的类针对其将维护的内部一致性与外部世界有一个契约,即人所共知的类不变式。
契约式设计
我在以前的一篇关于用 Nice 编程的文章中介绍过 DBC 的概念,Nice 是一门与 JRE 兼容的面向对象编程语言,它的特点是侧重于模块性、可表达性和安全性。有趣的是,Nice 并入了功能性开发技术,其中包括了一些在面向方面编程中的技术。功能性开发使得为方法指定前置条件和后置条件成为可能。
尽管 Nice 支持 DBC,但它与 Java™ 语言完全不同,因而很难将其用于开发。幸运的是,很多针对 Java 语言的库也都为 DBC 提供了方便。每个库都有其优点和缺点,每个库在 DBC 内针对 Java 语言进行构建的方法也不同;但近的一些新特性大都利用了 AOP 来更多地将 DBC 关注点包括进来,这些关注点基本上相当于方法的包装器。
前置条件在包装过的方法执行前击发,后置条件在该方法完成后击发。使用 AOP 构建 DBC 结构的一个好处(请不要同该语言本身相混淆!)是:可以在不需要 DBC 关注点的环境中将这些结构关掉(像断言能被关掉一样)。以横切的方式对待安全性关注点的真正妙处是:可以有效地重用 这些关注点。众所周知,重用是面向对象编程的一个基本原则。AOP 如此完美地补充了 OOP 难道不是一件极好的事情吗?
结合了 OVal 的 AOP
OVal 是一个通用的验证框架,它通过 AOP 支持简单的 DBC 结构并明确地允许:
为类字段和方法返回值指定约束条件
为结构参数指定约束条件
为方法参数指定约束条件
此外,OVal 还带来大量预定义的约束条件,这让创建新条件变得相当容易。
由于 OVal 使用 AspectJ 的 AOP 实现来为 DBC 概念定义建议,所以必须将 AspectJ 并入一个使用 OVal 的项目中。对于不熟悉 AOP 和 AspectJ 的人们来说,好消息是这不难实现,且使用 OVal (甚至是创建新的约束条件)并不需要真正对方面进行编码,只需编写一个简单的自引导程序即可,该程序会使 OVal 所附带的默认方面植入您的代码中。
在创建这个自引导程序方面前,要先下载 AspectJ。具体地说,您需要将 aspectjtools 和 aspectjrt JAR 文件并入您的构建中来编译所需的自引导程序方面并将其编入您的代码中。
自引导 AOP
下载了 AspectJ 后,下一步是创建一个可扩展 OVal GuardAspect 的方面。它本身不需要做什么,如清单 5 所示。请确保文件的扩展名以 .aj 结束,但不要试着用常规的 javac 对其进行编译。
清单 5. DefaultGuardAspect 自引导程序方面
import net.sf.oval.aspectj.GuardAspect;
public aspect DefaultGuardAspect extends GuardAspect{
public DefaultGuardAspect(){
super();
}
}
AspectJ 引入了一个 Ant 任务,称为 iajc,充当着 javac 的角色;此过程对方面进行编译并将其编入主体代码中。在本例中,只要是我指定了 OVal 约束条件的地方,在 OVal 代码中定义的逻辑会编入我的代码,进而充当起前置条件和后置条件。
请记住 iajc 代替了 javac。例如,清单 6 是我的 Ant build.xml 文件的一个代码片段,其中对代码进行了编译并把通过代码标注发现的所有 OVal 方面编入进来,如下所示:
清单 6. 用 AOP 编译的 Ant 构建文件片段
<target name="aspectjc" depends="get-deps">
<taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
<classpath>
<path refid="build.classpath" />
</classpath>
</taskdef>
<iajc destdir="${classesdir}" debug="on" source="1.5">
<classpath>
<path refid="build.classpath" />
</classpath>
<sourceroots>
<pathelement location="src/java" />
<pathelement location="test/java" />
</sourceroots>
</iajc>
</target>