November 26, 2024, Tuesday, 330

Defensive Programming With AOP

From NeoWiki

Revision as of 09:24, 5 March 2007 by Neo (Talk | contribs)
Jump to: navigation, search
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.

Tools clipart.png 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");
  }
}
Figure 1. The dreaded NullPointerException
Click for full sized figure.

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.

Tools clipart.png Tip: What about static analysis?
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

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.

Tools clipart.png Tip: What about assertions?
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.

Resources

Learn
  • "In pursuit of code quality: Don't be fooled by the coverage report" (Andrew Glover, developerWorks, January 2006): Are your test coverage measurements leading you astray? Find out in this first article in the new series!
  • "Test-first programming with Ruby" (Pat Eyler, developerWorks, May 2005): A simple exercise in test-first development, includes a discussion on refactoring.
  • "Measure test coverage with Cobertura" (Elliotte Rusty Harold, developerWorks, May 2005): A primer on using Cobertura for test coverage measurement.
  • The Java technology zone: Hundreds of articles about every aspect of Java programming.
Get products and technologies
  • JavaNCSS: A source measurement suite for the Java platform.
  • PMD: This popular open source tool scans Java code for problems.
  • CheckStyle: Another Java analysis tool from SourceForge.

About the author

Andrew Glover.jpg

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.

"When you have learned to snatch the error code from the trap frame, it will be time for you to leave.", thus spake the master programmer.