Defensive Programming With AOP
From NeoWiki
(→Defensive programming) |
m |
||
(6 intermediate revisions by one user not shown) | |||
Line 28: | Line 28: | ||
if(superclass != null && superclass.getName().equals("java.lang.Object")){ | if(superclass != null && superclass.getName().equals("java.lang.Object")){ | ||
− | return hier; | + | return hier; |
− | } else { | + | }else{ |
− | while((clzz.getSuperclass() != null) && | + | while((clzz.getSuperclass() != null) && |
(!clzz.getSuperclass().getName().equals("java.lang.Object"))){ | (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ | ||
clzz = clzz.getSuperclass(); | clzz = clzz.getSuperclass(); | ||
hier.addClass(clzz); | hier.addClass(clzz); | ||
− | } | + | } |
return hier; | return hier; | ||
} | } | ||
− | } | + | } |
Having just coded the method, I haven't yet noticed the defect, but because I'm a developer testing fanatic, I write a routine test using TestNG. What's more, I use TestNG's handy DataProvider feature, which allows me to create a generic test case and vary the parameters to it through another method. Running the test case defined in Listing 2 yields two passes! Everything is good to go, right? | Having just coded the method, I haven't yet noticed the defect, but because I'm a developer testing fanatic, I write a routine test using TestNG. What's more, I use TestNG's handy DataProvider feature, which allows me to create a generic test case and vary the parameters to it through another method. Running the test case defined in Listing 2 yields two passes! Everything is good to go, right? | ||
Line 52: | Line 52: | ||
public Object[][] dataValues(){ | public Object[][] dataValues(){ | ||
return new Object[][]{ | return new Object[][]{ | ||
− | { Vector.class, new String[] | + | { Vector.class, new String[] {"java.util.AbstractList", "java.util.AbstractCollection"} }, |
− | + | ||
{ String.class, new String[] {} } | { String.class, new String[] {} } | ||
}; | }; | ||
Line 75: | Line 74: | ||
Object verification is a classic defensive programming strategy for dealing with uncertainty. Accordingly, I add a check to verify whether clzz is null, as shown in Listing 3. If the value turns out to be null, I then throw a RuntimeException to alert everyone of the potential problem. | Object verification is a classic defensive programming strategy for dealing with uncertainty. Accordingly, I add a check to verify whether clzz is null, as shown in Listing 3. If the value turns out to be null, I then throw a RuntimeException to alert everyone of the potential problem. | ||
+ | |||
+ | {{tip|tip='''What about static analysis?'''<br />Static analysis tools like FindBugs examine class or JAR files looking for potential problems by matching bytecode against a list of bug patterns. Running FindBugs against the sample code did not uncover the NullPointerException found in Listing 1.}} | ||
'''Listing 3. Adding a check for null''' | '''Listing 3. Adding a check for null''' | ||
+ | public static Hierarchy buildHierarchy(Class clzz){ | ||
+ | if(clzz == null){ | ||
+ | throw new RuntimeException("Class parameter can not be null"); | ||
+ | } | ||
+ | |||
+ | Hierarchy hier = new Hierarchy(); | ||
+ | hier.setBaseClass(clzz); | ||
+ | |||
+ | Class superclass = clzz.getSuperclass(); | ||
+ | |||
+ | if(superclass != null && superclass.getName().equals("java.lang.Object")){ | ||
+ | return hier; | ||
+ | }else{ | ||
+ | while((clzz.getSuperclass() != null) && | ||
+ | (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ | ||
+ | clzz = clzz.getSuperclass(); | ||
+ | hier.addClass(clzz); | ||
+ | } | ||
+ | return hier; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | Naturally, I also write a quick test case to verify my check actually averts NullPointerExceptions, as shown in Listing 4: | ||
+ | |||
+ | '''Listing 4. Verifying the null check''' | ||
+ | @Test(expectedExceptions={RuntimeException.class}) | ||
+ | public void verifyHierarchyNull() throws Exception{ | ||
+ | Class clzz = null; | ||
+ | HierarchyBuilder.buildHierarchy(null); | ||
+ | } | ||
+ | |||
+ | In this case, defensive programming seems to have done the trick. But there are some downsides to relying solely on this strategy. | ||
+ | |||
+ | {{tip|tip='''What about assertions?'''<br />Listing 3 uses a conditional to verify the value of clzz, though an assert would have worked just as well. With assertions, there is no need to specify a conditional, nor an exception clause. The defensive programming concern is all handled by the JVM assuming assertions have been enabled.}} | ||
+ | |||
+ | ===Downsides of defense=== | ||
+ | |||
+ | While defensive programming effectively guarantees the condition of one method's input, it becomes repetitive if it is pervasive across a series of methods. Those familiar with aspect oriented programming, or AOP, will recognize this as a crosscutting concern, meaning that defensive programming techniques span horizontally across a code base. Many different objects employ these semantics, yet they have nothing to do with the object itself from a purely object-oriented view. | ||
+ | |||
+ | What's more, this crosscutting concern starts to bleed into the notion of design by contract (DBC). DBC is a technique for ensuring that all the components in a system do what they're meant to do by explicitly stating each component's intended functionality and expectations of the client in its interface. In DBC speak, a component's intended functionality is known as a postcondition, which is essentially the obligation of the component, while the expectation of the client is collectively known as the precondition. What's more, in pure DBC terms, a class that abides by DBC rules has a contract with the outside world about the internal consistency that it will maintain, which is known as the class invariant. | ||
+ | |||
+ | ==Design by contract== | ||
+ | |||
+ | I introduced the notion of DBC some time ago in an article about programming with Nice, which is an object-oriented, JRE-compatible programming language that has been formulated with an emphasis on modularity, expressiveness, and safety. Interestingly enough, Nice incorporates functional development techniques, including some of those found in aspect-oriented programming. Among other things, functional development makes it possible to specify preconditions and postconditions of a method. | ||
+ | |||
+ | While Nice supports DBC, it is fundamentally a different language from the Java™ language, which makes its incorporation into development shops somewhat challenging. Fortunately, there are libraries for the Java language that facilitate DBC. Each library has its pros and cons and even different methods for building in DBC for the Java language; however, recent additions to the field have taken advantage of AOP to facilitate weaving in DBC concerns, which essentially act as wrappers for a method. | ||
+ | |||
+ | A precondition is fired before the covered method is executed and the postcondition is fired after the method completes. One nice (not to be confused with the language!) thing about using AOP for building DBC constructs is that the constructs themselves can be turned off in environments where DBC concerns are unnecessary (just like assertions can be turned off). The real beauty of treating safety concerns in a crosscutting manner, however, is that you can effectively reuse them. And as we all know, reuse is a fundamental tenet of object-oriented programming. Isn't it slick how AOP complements OOP so nicely? | ||
+ | |||
+ | ==AOP with OVal== | ||
+ | |||
+ | OVal is a generic validation framework that supports simple DBC constructs via AOP and specifically enables you to: | ||
+ | |||
+ | * Specify constraints for class fields and method return values | ||
+ | * Specify constraints for constructor parameters | ||
+ | * Specify constraints for method parameters | ||
+ | |||
+ | What's more, OVal comes with a host of predefined constraints, and it makes creating new ones quite easy. | ||
+ | |||
+ | Because OVal uses AspectJ's implementation of AOP to define advices for DBC notions, you must incorporate AspectJ into a project that will use OVal. The good news for people not familiar with AOP and AspectJ is that the effort is minimal and using OVal (even for creating new constraints) does not require you to actually code aspects other than a simple bootstrap one, which forces the default aspects that come with OVal to hook into your code. | ||
+ | |||
+ | Before you can create this bootstrap aspect, you'll have to download AspectJ. Specifically, you'll need to incorporate aspectjtools and aspectjrt JARs into your build to compile the required bootstrap aspect and weave it into your code. | ||
+ | |||
+ | Bootstrapping AOP | ||
+ | |||
+ | After you've downloaded AspectJ, the next step is to create an aspect that extends OVal's GuardAspect. It need not do anything itself, as I've shown in Listing 5. Make sure the file extension ends in .aj, but don't attempt to compile it with the normal javac. | ||
+ | |||
+ | '''Listing 5. The DefaultGuardAspect bootstrap aspect''' | ||
+ | import net.sf.oval.aspectj.GuardAspect; | ||
+ | |||
+ | public aspect DefaultGuardAspect extends GuardAspect{ | ||
+ | public DefaultGuardAspect(){ | ||
+ | super(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | AspectJ comes with an Ant task, dubbed iajc, that acts like javac; however, this process compiles and weaves aspects into subject code. In this case, everywhere I specify an OVal constraint, the logic defined in OVal code will be weaved into my code, thus acting like preconditions and postconditions. | ||
+ | |||
+ | Keep in mind that iajc replaces javac. For example, Listing 6 is a snippet from my Ant build.xml file that compiles my code and weaves in any OVal aspects that are discovered via annotations in the code, which you'll see shortly: | ||
+ | |||
+ | '''Listing 6. An Ant build file snippet with AOP compilation''' | ||
+ | <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> | ||
+ | |||
+ | Now that I've set up the plumbing for OVal and bootstrapped the AOP process, I can begin to specify simple constraints for my code using Java 5 annotations. | ||
+ | |||
+ | ==OVal's reusable constraints== | ||
+ | |||
+ | To specify preconditions for a method with OVal, you must annotate method parameters. Accordingly, when a method that has been annotated with OVal constraints is invoked, OVal will verify the constraints before the method is actually executed. | ||
+ | |||
+ | In my case, I'd like to specify that the buildHierarchy method not be invoked with a null as the value for the Class parameter. Out of the box, OVal supports this constraint with the @NotNull annotation, which is specified before any desired parameter for a method. Note, too, that any class that wishes to use OVal constraints must also specify the @Guarded annotation at the class level as well, as I do in Listing 7: | ||
+ | |||
+ | '''Listing 7. OVal constraints in action''' | ||
+ | import net.sf.oval.annotations.Guarded; | ||
+ | import net.sf.oval.constraints.NotNull; | ||
+ | |||
+ | @Guarded | ||
+ | public class HierarchyBuilder { | ||
+ | |||
+ | public static Hierarchy buildHierarchy(@NotNull Class clzz){ | ||
+ | Hierarchy hier = new Hierarchy(); | ||
+ | hier.setBaseClass(clzz); | ||
+ | |||
+ | Class superclass = clzz.getSuperclass(); | ||
+ | |||
+ | if(superclass != null && superclass.getName().equals("java.lang.Object")){ | ||
+ | return hier; | ||
+ | }else{ | ||
+ | while((clzz.getSuperclass() != null) && | ||
+ | (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ | ||
+ | clzz = clzz.getSuperclass(); | ||
+ | hier.addClass(clzz); | ||
+ | } | ||
+ | return hier; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | Specifying this constraint through annotations means I no longer need to litter my code with repetitive conditionals that check for null and throw an exception on finding it. This logic is now handled by OVal, which acts much the same -- in fact, if a constraint is violated, OVal throws a ConstraintsViolatedException, which is a subclass of RuntimeException. | ||
+ | |||
+ | Of course, my next step is to compile the HierarchyBuilder class and my corresponding DefaultGuardAspect from Listing 5. I do this using the iajc task from Listing 6 so that OVal's behavior can be weaved into my code. | ||
+ | |||
+ | Next, I update the test case from Listing 4 to verify that a ConstraintsViolatedException is thrown, as shown in Listing 8: | ||
+ | |||
+ | '''Listing 8. Verifying ConstraintsViolatedException has been thrown''' | ||
+ | @Test(expectedExceptions={ConstraintsViolatedException.class}) | ||
+ | public void verifyHierarchyNull() throws Exception{ | ||
+ | Class clzz = null; | ||
+ | HierarchyBuilder.buildHierarchy(clzz); | ||
+ | } | ||
+ | |||
+ | ===Specifying postconditions=== | ||
+ | |||
+ | As you can see, specifying preconditions is actually quite easy, as is the process of specifying postconditions. For example, if I wanted to guarantee to all callers of the buildHierarchy that it wouldn't return null (and consequently that they wouldn't need to check for it), I could place a @NotNull annotation above the method declaration, as shown in Listing 9: | ||
+ | |||
+ | '''Listing 9. Postconditions in OVal''' | ||
+ | @NotNull | ||
+ | public static Hierarchy buildHierarchy(@NotNull Class clzz){ | ||
+ | //method body | ||
+ | } | ||
+ | |||
+ | Now, @NotNull is by no means the only constraint OVal provides, but I've found it quite useful in limiting those nasty NullPointerExceptions, or at least exposing them quickly. | ||
+ | |||
+ | ===More OVal constraints=== | ||
+ | |||
+ | OVal also supports a means to prevalidate class members before or after a method call. This mechanism has the benefit of limiting repeated conditionals testing for certain constraints, such as collection sizes or the previously discussed not-null condition. | ||
+ | |||
+ | For example, in Listing 10, I've defined an Ant task that builds a report for a class hierarchy, using the HierarchyBuilder. Note how the execute() method calls validate, which in turn verifies that the fileSet class member has values in it; otherwise, an exception is thrown because the report can't function without any classes to evaluate. | ||
+ | |||
+ | '''Listing 10. HierarchyBuilderTask with conditional checking''' | ||
+ | public class HierarchyBuilderTask extends Task { | ||
+ | private Report report; | ||
+ | private List fileSet; | ||
+ | |||
+ | private void validate() throws BuildException{ | ||
+ | if(!(this.fileSet.size() > 0)){ | ||
+ | throw new BuildException("must supply classes to evaluate"); | ||
+ | } | ||
+ | if(this.report == null){ | ||
+ | this.log("no report defined, printing XML to System.out"); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public void execute() throws BuildException { | ||
+ | validate(); | ||
+ | String[] classes = this.getQualifiedClassNames(this.fileSet); | ||
+ | Hierarchy[] hclz = new Hierarchy[classes.length]; | ||
+ | |||
+ | try{ | ||
+ | for(int x = 0; x < classes.length; x++){ | ||
+ | hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]); | ||
+ | } | ||
+ | BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); | ||
+ | this.handleReportCreation(xmler); | ||
+ | }catch(ClassNotFoundException e){ | ||
+ | throw new BuildException("Unable to load class check classpath! " + e.getMessage()); | ||
+ | } | ||
+ | } | ||
+ | //more methods below.... | ||
+ | } | ||
+ | |||
+ | Because I'm using OVal, I can do the following: | ||
+ | |||
+ | * Specify a constraint on the fileSet class member, which ensures the size is always at least 1 or more using the @Size annotation. | ||
+ | * Ensure that this constraint is verified before the execute() method is invoked using the @PreValidateThis annotation. | ||
+ | |||
+ | These two steps allow me to effectively remove the conditional check in the validate() method and let OVal do it for me, as shown in Listing 11: | ||
+ | |||
+ | '''Listing 11. A reworked HierarchyBuilderTask without conditional checking''' | ||
+ | @Guarded | ||
+ | public class HierarchyBuilderTask extends Task { | ||
+ | private Report report; | ||
+ | |||
+ | @Size(min = 1) | ||
+ | private List fileSet; | ||
+ | |||
+ | private void validate() throws BuildException { | ||
+ | if (this.report == null) { | ||
+ | this.log("no report defined, printing XML to System.out"); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | @PreValidateThis | ||
+ | public void execute() throws BuildException { | ||
+ | validate(); | ||
+ | String[] classes = this.getQualifiedClassNames(this.fileSet); | ||
+ | Hierarchy[] hclz = new Hierarchy[classes.length]; | ||
+ | |||
+ | try{ | ||
+ | for(int x = 0; x < classes.length; x++){ | ||
+ | hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]); | ||
+ | } | ||
+ | BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); | ||
+ | this.handleReportCreation(xmler); | ||
+ | }catch(ClassNotFoundException e){ | ||
+ | throw new BuildException("Unable to load class check classpath! " + e.getMessage()); | ||
+ | } | ||
+ | } | ||
+ | //more methods below.... | ||
+ | } | ||
+ | |||
+ | Any time execute() is invoked in Listing 11 (which is done through Ant), the fileSet member is verified by OVal. If it is empty, meaning someone didn't specify any classes to evaluate, a ConstraintsViolatedException is thrown. The exception halts the process much like the original code, which threw a BuildException. | ||
+ | ==Concluding argument== | ||
+ | Defensive programming constructs have prevented many a defect, but the constructs themselves tend to litter code with repetitive logic. Combining defensive programming techniques with aspect-oriented programming (through design by contract) is one way to keep the strong line of defense without all the repetitive coding. | ||
+ | OVal isn't the only DBC library available, and in fact its DBC constructs are fairly limiting compared to other frameworks (for example, it doesn't offer an easy way to specify class invariants). On the other hand, OVal's ease of use and wide selection of constraints make it a viable option for adding validation constraints to code with little effort. Besides, creating custom constraints is amazingly simple with OVal, so stop adding conditional checks and employ the power of AOP! | ||
==Resources== | ==Resources== | ||
;Learn | ;Learn | ||
− | * "[http://www.ibm.com/developerworks | + | * "[http://www.ibm.com/developerworks/library/j-aopsc/ AOP banishes the tight-coupling blues]" (Andrew Glover, developerWorks, February 2004): See for yourself how one of AOP's functional design concepts -- static crosscutting -- can turn what might be a tangled mass of tightly coupled code into a powerful, extensible enterprise application. |
− | * "[http://www.ibm.com/developerworks/ | + | * "[http://www.ibm.com/developerworks/library/j-alj10064.html alt.lang.jre: Twice as Nice]" (Andrew Glover, developerWorks, October 2004): Regular contributor and all around "Nice" guy Andrew Glover walks you through some of the most exciting features of Nice, including DBC features. |
− | * "[http://www.ibm.com/developerworks/java/library/j- | + | * "[http://www.ibm.com/developerworks/java/library/j-ceaop/ Contract enforcement with AOP]" (Filippo Diotalevi, developerWorks, July 2004): Filippo Diotalevi shows how AOP can help you define clear contracts between components while keeping your code clean and flexible. |
− | * [http://www.ibm.com/developerworks/java/ | + | * [http://www.testearly.com/2006/12/31/limiting-conditional-complexity-with-aop/ Limiting conditional complexity with AOP] (testearly.com, December 2006): Another look at OVal's easy usage for DBC constructs. |
+ | * "[http://www.ibm.com/developerworks/library/j-aopwork17.html AOP@Work: Component design with Contract4J]" (Dean Wampler, developerWorks, April 2006): Dean Wampler introduces Contract4J, a DBC tool that specifies contracts using Java 5 annotations and evaluates them at run time using AspectJ aspects. | ||
+ | * "[http://www.ibm.com/developerworks/java/library/j-mer0219.html Magic with Merlin: Working with assertions]" (John Zukowski, developerWorks, February 2002): John Zukowski walks you through the basics of adding assertion-checking to your code as well as enabling and disabling assertions. | ||
+ | * "[http://www.ibm.com/developerworks/java/library/j-testng/ TestNG makes Java unit testing a breeze]" (Filippo Diotalevi, developerWorks, January 2005): Filippo Diotalevi introduces TestNG, a new framework for testing Java applications. | ||
+ | * "[http://www.ibm.com/developerworks/java/library/j-cq08296/ In pursuit of code quality: JUnit 4 vs. TestNG]" (Andrew Glover, developerWorks, August 2006): Andrew Glover considers what's unique about each framework and reveals three high-level testing features you'll still find only in TestNG. | ||
+ | * [http://www.ibm.com/developerworks/views/java/libraryview.jsp?search_by=code+quality: In pursuit of code quality series] (Andrew Glover, developerWorks): Learn more about code metrics, test frameworks, and writing quality-focused code. | ||
;Get products and technologies | ;Get products and technologies | ||
− | * [http:// | + | * [http://oval.sourceforge.net/ Download OVal]: A generic validation framework for any kind of Java objects. |
− | * [http:// | + | * [http://www.testng.org/ Download TestNG]: A testing framework inspired by JUnit and NUnit but introducing some new features that make it more powerful and easier to use. |
− | * [http:// | + | * [http://eclipse.org/aspectj Download AspectJ]: A seamless aspect-oriented extension to the Java programming language. |
==About the author== | ==About the author== | ||
Line 99: | Line 345: | ||
Andrew Glover is president of [http://www.stelligent.com/ Stelligent Incorporated], which helps companies address software quality with effective developer testing strategies and continuous integration techniques that enable teams to monitor code quality early and often. Check out [http://www.thediscoblog.com/publications/ Andy's blog] for a list of his publications. | Andrew Glover is president of [http://www.stelligent.com/ Stelligent Incorporated], which helps companies address software quality with effective developer testing strategies and continuous integration techniques that enable teams to monitor code quality early and often. Check out [http://www.thediscoblog.com/publications/ Andy's blog] for a list of his publications. | ||
− | + | [[Category:Programming]] |
Latest revision as of 16:18, 5 March 2007
- OVal takes the legwork out of writing repetitive conditionals
Andrew Glover, President, Stelligent Incorporated
30 Jan 2007
- While defensive programming effectively guarantees the condition of a method's input, it becomes repetitive if it is pervasive across a series of methods. This month, Andrew Glover shows you an easier way to add reusable validation constraints to your code using the power of AOP, design by contract, and a handy library called OVal.
The major downside to developer testing is that the vast majority of tests exercise sunny-day scenarios. Defects rarely occur for these situations -- it's the edge cases that usually cause problems.
What's an edge case? It's the situation where, for instance, someone passes a null value to a method not coded to handle nulls. Many developers fail to test for such scenarios because they don't make much sense. But sense or no sense, these things happen, and then a NullPointerException is thrown and your whole program blows up.
This month, I suggest a multifaceted approach to dealing with the less predictable defects in your code. Find out what happens when you combine defensive programming, design by contract, and an easy-to-use generic validation framework called OVal.
Tip: Download OVal and AspectJ You need to download OVal and AspectJ to implement the programming solution described in this article. See Resources to download these technologies now and follow along with the examples. |
Contents |
Exposing the enemy
The code in Listing 1 builds a class hierarchy for a given Class object (omitting java.lang.Object because everything ultimately extends it). If you look carefully, however, you'll notice a potential defect waiting to be exposed because of assumptions in the method regarding object values.
Listing 1. A method without checks for null
public static Hierarchy buildHierarchy(Class clzz){ Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); Class superclass = clzz.getSuperclass(); if(superclass != null && superclass.getName().equals("java.lang.Object")){ return hier; }else{ while((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } }
Having just coded the method, I haven't yet noticed the defect, but because I'm a developer testing fanatic, I write a routine test using TestNG. What's more, I use TestNG's handy DataProvider feature, which allows me to create a generic test case and vary the parameters to it through another method. Running the test case defined in Listing 2 yields two passes! Everything is good to go, right?
Listing 2. A TestNG test verifying two values
import java.util.Vector; import static org.testng.Assert.assertEquals; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class BuildHierarchyTest { @DataProvider(name = "class-hierarchies") public Object[][] dataValues(){ return new Object[][]{ { Vector.class, new String[] {"java.util.AbstractList", "java.util.AbstractCollection"} }, { String.class, new String[] {} } }; } @Test(dataProvider = "class-hierarchies"}) public void verifyHierarchies(Class clzz, String[] names) throws Exception{ Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz); assertEquals(hier.getHierarchyClassNames(), names, "values were not equal"); } }
I still haven't spotted the defect, but something about the code is bothering me. What if someone inadvertently passes in a null value for the Class parameter? The call clzz.getSuperclass() in the fourth line of Listing 1 would throw a NullPointerException, wouldn't it?
Testing my theory is easy; I don't even have to start from scratch. I simply add {null, null} to the multidimensional Object array in the dataValues method of the original BuildHierarchyTest (in Listing 1) and run it again. Sure enough, I get the nasty NullPointerException shown in Figure 1:
Defensive programming
Once I've exposed the issue, my next step is to come up with a strategy for defeating it. The problem is that I can't control the kind of input this method will receive. For this type of problem, developers often employ defensive programming techniques that aim to catch potential errors before they wreak havoc.
Object verification is a classic defensive programming strategy for dealing with uncertainty. Accordingly, I add a check to verify whether clzz is null, as shown in Listing 3. If the value turns out to be null, I then throw a RuntimeException to alert everyone of the potential problem.
Listing 3. Adding a check for null
public static Hierarchy buildHierarchy(Class clzz){ if(clzz == null){ throw new RuntimeException("Class parameter can not be null"); } Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); Class superclass = clzz.getSuperclass(); if(superclass != null && superclass.getName().equals("java.lang.Object")){ return hier; }else{ while((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } }
Naturally, I also write a quick test case to verify my check actually averts NullPointerExceptions, as shown in Listing 4:
Listing 4. Verifying the null check
@Test(expectedExceptions={RuntimeException.class}) public void verifyHierarchyNull() throws Exception{ Class clzz = null; HierarchyBuilder.buildHierarchy(null); }
In this case, defensive programming seems to have done the trick. But there are some downsides to relying solely on this strategy.
Downsides of defense
While defensive programming effectively guarantees the condition of one method's input, it becomes repetitive if it is pervasive across a series of methods. Those familiar with aspect oriented programming, or AOP, will recognize this as a crosscutting concern, meaning that defensive programming techniques span horizontally across a code base. Many different objects employ these semantics, yet they have nothing to do with the object itself from a purely object-oriented view.
What's more, this crosscutting concern starts to bleed into the notion of design by contract (DBC). DBC is a technique for ensuring that all the components in a system do what they're meant to do by explicitly stating each component's intended functionality and expectations of the client in its interface. In DBC speak, a component's intended functionality is known as a postcondition, which is essentially the obligation of the component, while the expectation of the client is collectively known as the precondition. What's more, in pure DBC terms, a class that abides by DBC rules has a contract with the outside world about the internal consistency that it will maintain, which is known as the class invariant.
Design by contract
I introduced the notion of DBC some time ago in an article about programming with Nice, which is an object-oriented, JRE-compatible programming language that has been formulated with an emphasis on modularity, expressiveness, and safety. Interestingly enough, Nice incorporates functional development techniques, including some of those found in aspect-oriented programming. Among other things, functional development makes it possible to specify preconditions and postconditions of a method.
While Nice supports DBC, it is fundamentally a different language from the Java™ language, which makes its incorporation into development shops somewhat challenging. Fortunately, there are libraries for the Java language that facilitate DBC. Each library has its pros and cons and even different methods for building in DBC for the Java language; however, recent additions to the field have taken advantage of AOP to facilitate weaving in DBC concerns, which essentially act as wrappers for a method.
A precondition is fired before the covered method is executed and the postcondition is fired after the method completes. One nice (not to be confused with the language!) thing about using AOP for building DBC constructs is that the constructs themselves can be turned off in environments where DBC concerns are unnecessary (just like assertions can be turned off). The real beauty of treating safety concerns in a crosscutting manner, however, is that you can effectively reuse them. And as we all know, reuse is a fundamental tenet of object-oriented programming. Isn't it slick how AOP complements OOP so nicely?
AOP with OVal
OVal is a generic validation framework that supports simple DBC constructs via AOP and specifically enables you to:
- Specify constraints for class fields and method return values
- Specify constraints for constructor parameters
- Specify constraints for method parameters
What's more, OVal comes with a host of predefined constraints, and it makes creating new ones quite easy.
Because OVal uses AspectJ's implementation of AOP to define advices for DBC notions, you must incorporate AspectJ into a project that will use OVal. The good news for people not familiar with AOP and AspectJ is that the effort is minimal and using OVal (even for creating new constraints) does not require you to actually code aspects other than a simple bootstrap one, which forces the default aspects that come with OVal to hook into your code.
Before you can create this bootstrap aspect, you'll have to download AspectJ. Specifically, you'll need to incorporate aspectjtools and aspectjrt JARs into your build to compile the required bootstrap aspect and weave it into your code.
Bootstrapping AOP
After you've downloaded AspectJ, the next step is to create an aspect that extends OVal's GuardAspect. It need not do anything itself, as I've shown in Listing 5. Make sure the file extension ends in .aj, but don't attempt to compile it with the normal javac.
Listing 5. The DefaultGuardAspect bootstrap aspect
import net.sf.oval.aspectj.GuardAspect; public aspect DefaultGuardAspect extends GuardAspect{ public DefaultGuardAspect(){ super(); } }
AspectJ comes with an Ant task, dubbed iajc, that acts like javac; however, this process compiles and weaves aspects into subject code. In this case, everywhere I specify an OVal constraint, the logic defined in OVal code will be weaved into my code, thus acting like preconditions and postconditions.
Keep in mind that iajc replaces javac. For example, Listing 6 is a snippet from my Ant build.xml file that compiles my code and weaves in any OVal aspects that are discovered via annotations in the code, which you'll see shortly:
Listing 6. An Ant build file snippet with AOP compilation
<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>
Now that I've set up the plumbing for OVal and bootstrapped the AOP process, I can begin to specify simple constraints for my code using Java 5 annotations.
OVal's reusable constraints
To specify preconditions for a method with OVal, you must annotate method parameters. Accordingly, when a method that has been annotated with OVal constraints is invoked, OVal will verify the constraints before the method is actually executed.
In my case, I'd like to specify that the buildHierarchy method not be invoked with a null as the value for the Class parameter. Out of the box, OVal supports this constraint with the @NotNull annotation, which is specified before any desired parameter for a method. Note, too, that any class that wishes to use OVal constraints must also specify the @Guarded annotation at the class level as well, as I do in Listing 7:
Listing 7. OVal constraints in action
import net.sf.oval.annotations.Guarded; import net.sf.oval.constraints.NotNull; @Guarded public class HierarchyBuilder { public static Hierarchy buildHierarchy(@NotNull Class clzz){ Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); Class superclass = clzz.getSuperclass(); if(superclass != null && superclass.getName().equals("java.lang.Object")){ return hier; }else{ while((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } } }
Specifying this constraint through annotations means I no longer need to litter my code with repetitive conditionals that check for null and throw an exception on finding it. This logic is now handled by OVal, which acts much the same -- in fact, if a constraint is violated, OVal throws a ConstraintsViolatedException, which is a subclass of RuntimeException.
Of course, my next step is to compile the HierarchyBuilder class and my corresponding DefaultGuardAspect from Listing 5. I do this using the iajc task from Listing 6 so that OVal's behavior can be weaved into my code.
Next, I update the test case from Listing 4 to verify that a ConstraintsViolatedException is thrown, as shown in Listing 8:
Listing 8. Verifying ConstraintsViolatedException has been thrown
@Test(expectedExceptions={ConstraintsViolatedException.class}) public void verifyHierarchyNull() throws Exception{ Class clzz = null; HierarchyBuilder.buildHierarchy(clzz); }
Specifying postconditions
As you can see, specifying preconditions is actually quite easy, as is the process of specifying postconditions. For example, if I wanted to guarantee to all callers of the buildHierarchy that it wouldn't return null (and consequently that they wouldn't need to check for it), I could place a @NotNull annotation above the method declaration, as shown in Listing 9:
Listing 9. Postconditions in OVal
@NotNull public static Hierarchy buildHierarchy(@NotNull Class clzz){ //method body }
Now, @NotNull is by no means the only constraint OVal provides, but I've found it quite useful in limiting those nasty NullPointerExceptions, or at least exposing them quickly.
More OVal constraints
OVal also supports a means to prevalidate class members before or after a method call. This mechanism has the benefit of limiting repeated conditionals testing for certain constraints, such as collection sizes or the previously discussed not-null condition.
For example, in Listing 10, I've defined an Ant task that builds a report for a class hierarchy, using the HierarchyBuilder. Note how the execute() method calls validate, which in turn verifies that the fileSet class member has values in it; otherwise, an exception is thrown because the report can't function without any classes to evaluate.
Listing 10. HierarchyBuilderTask with conditional checking
public class HierarchyBuilderTask extends Task { private Report report; private List fileSet; private void validate() throws BuildException{ if(!(this.fileSet.size() > 0)){ throw new BuildException("must supply classes to evaluate"); } if(this.report == null){ this.log("no report defined, printing XML to System.out"); } } public void execute() throws BuildException { validate(); String[] classes = this.getQualifiedClassNames(this.fileSet); Hierarchy[] hclz = new Hierarchy[classes.length]; try{ for(int x = 0; x < classes.length; x++){ hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]); } BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); this.handleReportCreation(xmler); }catch(ClassNotFoundException e){ throw new BuildException("Unable to load class check classpath! " + e.getMessage()); } } //more methods below.... }
Because I'm using OVal, I can do the following:
- Specify a constraint on the fileSet class member, which ensures the size is always at least 1 or more using the @Size annotation.
- Ensure that this constraint is verified before the execute() method is invoked using the @PreValidateThis annotation.
These two steps allow me to effectively remove the conditional check in the validate() method and let OVal do it for me, as shown in Listing 11:
Listing 11. A reworked HierarchyBuilderTask without conditional checking
@Guarded public class HierarchyBuilderTask extends Task { private Report report; @Size(min = 1) private List fileSet; private void validate() throws BuildException { if (this.report == null) { this.log("no report defined, printing XML to System.out"); } } @PreValidateThis public void execute() throws BuildException { validate(); String[] classes = this.getQualifiedClassNames(this.fileSet); Hierarchy[] hclz = new Hierarchy[classes.length]; try{ for(int x = 0; x < classes.length; x++){ hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]); } BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); this.handleReportCreation(xmler); }catch(ClassNotFoundException e){ throw new BuildException("Unable to load class check classpath! " + e.getMessage()); } } //more methods below.... }
Any time execute() is invoked in Listing 11 (which is done through Ant), the fileSet member is verified by OVal. If it is empty, meaning someone didn't specify any classes to evaluate, a ConstraintsViolatedException is thrown. The exception halts the process much like the original code, which threw a BuildException.
Concluding argument
Defensive programming constructs have prevented many a defect, but the constructs themselves tend to litter code with repetitive logic. Combining defensive programming techniques with aspect-oriented programming (through design by contract) is one way to keep the strong line of defense without all the repetitive coding.
OVal isn't the only DBC library available, and in fact its DBC constructs are fairly limiting compared to other frameworks (for example, it doesn't offer an easy way to specify class invariants). On the other hand, OVal's ease of use and wide selection of constraints make it a viable option for adding validation constraints to code with little effort. Besides, creating custom constraints is amazingly simple with OVal, so stop adding conditional checks and employ the power of AOP!
Resources
- Learn
- "AOP banishes the tight-coupling blues" (Andrew Glover, developerWorks, February 2004): See for yourself how one of AOP's functional design concepts -- static crosscutting -- can turn what might be a tangled mass of tightly coupled code into a powerful, extensible enterprise application.
- "alt.lang.jre: Twice as Nice" (Andrew Glover, developerWorks, October 2004): Regular contributor and all around "Nice" guy Andrew Glover walks you through some of the most exciting features of Nice, including DBC features.
- "Contract enforcement with AOP" (Filippo Diotalevi, developerWorks, July 2004): Filippo Diotalevi shows how AOP can help you define clear contracts between components while keeping your code clean and flexible.
- Limiting conditional complexity with AOP (testearly.com, December 2006): Another look at OVal's easy usage for DBC constructs.
- "AOP@Work: Component design with Contract4J" (Dean Wampler, developerWorks, April 2006): Dean Wampler introduces Contract4J, a DBC tool that specifies contracts using Java 5 annotations and evaluates them at run time using AspectJ aspects.
- "Magic with Merlin: Working with assertions" (John Zukowski, developerWorks, February 2002): John Zukowski walks you through the basics of adding assertion-checking to your code as well as enabling and disabling assertions.
- "TestNG makes Java unit testing a breeze" (Filippo Diotalevi, developerWorks, January 2005): Filippo Diotalevi introduces TestNG, a new framework for testing Java applications.
- "In pursuit of code quality: JUnit 4 vs. TestNG" (Andrew Glover, developerWorks, August 2006): Andrew Glover considers what's unique about each framework and reveals three high-level testing features you'll still find only in TestNG.
- In pursuit of code quality series (Andrew Glover, developerWorks): Learn more about code metrics, test frameworks, and writing quality-focused code.
- Get products and technologies
- Download OVal: A generic validation framework for any kind of Java objects.
- Download TestNG: A testing framework inspired by JUnit and NUnit but introducing some new features that make it more powerful and easier to use.
- Download AspectJ: A seamless aspect-oriented extension to the Java programming language.
About the author
Andrew Glover is president of Stelligent Incorporated, which helps companies address software quality with effective developer testing strategies and continuous integration techniques that enable teams to monitor code quality early and often. Check out Andy's blog for a list of his publications.