may creep in and they may be hard to find. "Design by Contract" is a set of techniques that
let you specify conditions on method parameters, return values, etc, in such a way that they
can be associated with interfaces and that the execution of these statements is assured by the
runtime system, freeing the developer from the tedious tasks of throwing
IllegalArgumentExceptions or assertions.
To implement such techniques, you can use some off the shelf frameworks, like jContractor, but it's easy to roll out your own. You need only three components:
- you need to be able to attach conditions to classes and methods, without cluttering the code. For this, Java annotations are perfect
- since we're not in Java code proper, we need a way to run our conditions, so we need a language for them. Well, why have another language, just use a Java scripting languages like Beanshell.
- finally, the Beanshell code needs to be evaluated at runtime before or after methods calls. This is what aspect are for, right? We'll use AspectJ for that.
So, let's see a concrete example. Suppose we implement a Stack (yeah, right, I don't have a lamer example).
We start with the push method:
public interface IStack
void push(T o);
}
Ok, but we don't like to have null values in this stack. So we want to specify in the interface definition that the first parameter cannot be null:
@ContractMethodAnnotation (
pre = {"p0!=null"}
)
void push(T o);
We cannot access the names of the method parameters at runtime, so we use a simple convention: p0 is the first parameter, etc. You can also define post conditions that can access the return value using the name "return".
Notice that we specify conditions on the interface, but that we will want the checks to apply to all its implementations.
Conditions can access methods and variables of the class you define them on, but be careful, it's easy to create circular references where two method call each other:
boolean isEmpty();
@ContractMethodAnnotation (
post = {"result>=0"}
)
int size();
It would be tempting to define a condition that isEmpty is true if size()==0 and that size()==0 if isEmpty is true... What you can do is define a class invariant condition: a condition that is always true of an object (even if it can transiently be false, it is true before and after public method invocation).
@ContractClassAnnotation (
invariants={"(isEmpty() && size()==0) || (!isEmpty() && size()>0)"}
)
public interface IStack
The implementation of the aspect is not very complicated. We define pointcuts for public methods that will check class invariants and for annotated methods that will check pre and post conditions. We use the BeanShell Interpreter object to parse and run the condition code. Thanks to the importObject Beanshell command we can run the code as if all methods referenced by the annotation code referenced the current instance.
When we encounter a failure, we can either use an assertion or throw a RuntimeException.
This can be set through the ContractAspect.setUseAsserts method. For fun, I have defined a way to set it through a bean shell configuration file, that is loaded by another aspect, that executes when the ContractAspect is statically initialized.
The code can be found here: ContractAspect.aj is the aspect implementation, ContractClassAnnotation.java and ContractMethodAnnotation.java are the definition of the annotations. SettingsAspect.aj is the aspect that reads the configuration file and turn assertions on or off. An example of a configuration file is here. As you see, it's only one line of Java code.
The Stack example can be found here, with an implementation and a unit test.
6 comments:
Keen stuff. What do you think the chances are of adding "agents" per the Eiffel DbC (http://www.artima.com/intv/contracts3.html) approach? Or does it sorta already happen because you can hack up bsh to do whatever you want?
I'd say you can implement "agents" in Java (static methods, say) and call them from the BSH code.
I fixed the links, they should work for you now. Thanks for letting me know.
Well, the problem in using a general scripting language to specify constraints' code is that it can not be checked at compile time. I developed a sort of stripped java that I can verify at compile time and generate errors that are reported into the IDE (thanks to Pluggable Annotations Processing API) and then instrument the code using a Java agent. If you are interested look at http://sf.net/projects/jdefprog or write to me.
Post a Comment