Saturday, August 8, 2009

Changing Return Values to Exceptions, Java/AspectJ Edition

In the last couple of posts, I've been talking about APIs that communicate errors via return values, and how to map those to exceptions. As we saw most recently, one reasonably effective approach in C++ makes use of macros, but it requires a degree of development discipline, and the resulting code remains somewhat unsatisfactory.

In this post, I will present an approach for Java that mitigates these issues. Specifically, you can neatly map return values to exceptions using AspectJ.

(Note that I am using Eclipse for this work. It's great that AspectJ is now an Eclipse project, because it can be incredibly frustrating to use without solid IDE support.)

I'll start with some Java math utility routines that are roughly equivalent to what I was working with in C++. One difference is that in Java, I don't need a custom sqrt() wrapper because the Math.sqrt() method already uses a return value, Double.NaN, to indicate error.
package com.scottmcmaster365.math;

public class MyMathUtils
{
  public static double radiusFromArea(double area)
  {
    return Math.sqrt(area / 3.14);
  }

  public static double circumferenceFromRadius(double radius)
  {
    return 2 * 3.14 * radius;
  }
}
I want to wire up an aspect that runs when these functions are returning, checks for NaN, and throws an exception. With a bit of understanding of AspectJ, this is quite simple:
package com.scottmcmaster365.aspects;

import com.scottmcmaster365.math.MyMathUtils;

/**
 * This aspect maps double NaN return values to runtime exceptions.
 * (For instructional purposes only.)
 * @author Scott McMaster
 *
 */
public aspect MapNaNResultToExceptionAspect
{
  pointcut mathClass() : within(MyMathUtils);
 
  pointcut mathMethods() : mathClass() && execution(* *(..));
 
  after () returning (double d) : mathMethods()
  {
    if( Double.isNaN(d) )
    {
      throw new RuntimeException("Result is NaN");
    }
  }
}
Hopefully even if you're not familiar with AspectJ, that makes at least a little sense to you. Otherwise, I suggest checking out a good tutorial. But in a nutshell, the pointcuts pick out the methods in the MyMathUtils class, and the after advice grabs the return value and allows us to inject code at compile-time.

With this aspect in effect, the client for finding the circumference given the area of a circle will throw a RuntimeException during the call to radiusFromArea():
final double TESTAREA = -1.0;
double radius = MyMathUtils.radiusFromArea(TESTAREA);
double circumference = MyMathUtils.circumferenceFromRadius(radius);
System.out.println(circumference);
That's exactly what I was after: Clean code that doesn't have to check return values explicitly or via macros. Score one for AspectJ. In fact, by making a very minor change to the pointcuts and advice spec, I can even fire the aspect at the Math.sqrt() call site, which (in a more complicated example) narrow the defect down even further. I'll leave this as an exercise for the reader unless someone comments that they want to see it.

No comments: