Implement Design by Contract for Java using dynamic proxies

Write bug-free code with the DBCProxy framework

1 2 Page 2
Page 2 of 2

You enable the DBC runtime environment at the package level. In the Stack example, we enable DBC for all assertion types using the following settings in the dbc.properties file:

dbc.precondition=dbc.test
dbc.postcondition=dbc.test
dbc.invariant=dbc.test

Test the DBC-enabled stack

To create a Stack instance and validate the assertions defined, you need to implement the Stack interface.

Listing 2 shows Java class StackImpl, which implements the Stack interface. Notice that the package is declared as dbc.test, which is the package identified in the dbc.properties file described in the previous section:

Listing 2. StackImpl class

1 package dbc.test;
2
3 import java.util.*;
4
5 public class StackImpl implements Stack 
6 {
7 public StackImpl(int capacity)
8 {
9 this.capacity = capacity;
10 vector = new Vector(capacity);
11 }
12
13 public int capacity()
14 {
15 return capacity;
16 }
17
18 public int count()
19 {
20 return vector.size();
21 }
22
23 public Object item()
24 {
25 return vector.elementAt(vector.size() - 1);
26 }
27
28 public boolean empty()
29 {
30 return count() == 0;
31 }
32
33
34 public boolean full()
35 {
36 return count() == capacity();
37 }
38
39 public void put(Object obj)
40 {
41 vector.addElement(obj);
42 }
43
44 public void remove()
45 {
46 vector.removeElementAt(vector.size() - 1);
47 }
48
49 private int capacity;
50 private Vector vector;
51}

With this Stack implementation, we create a DBC-enabled Stack instance. (StackImpl could include assertions, so assertion files could require generation for those assertions as well.)

To create that DBC-enabled instance, you use the dbc.DBC class from Figure 1. As stated earlier, this class represents DBCProxy's runtime environment. dbc.DBC has a public method with the following signature:

public static Object newInstance(Object obj) throws Throwable

The newInstance() method is almost the complete DBCProxy API. The argument passed to newInstance() is the primary object (in this case, StackImpl). You can safely cast the returned object to the Stack interface because StackImpl implements the interface; newInstance actually returns a dynamic proxy that implements all interfaces that StackImpl implements.

The following code shows how to create a DBC-enabled StackImpl instance:

Stack stack = (Stack)DBC.newInstance(new StackImpl(10));

That's it. In the code above, stack is a Stack instance that enforces the preconditions, postconditions, and invariant defined by the assertion tags. Figure 3 shows a simplified version of what happens when you instantiate the DBC-enabled stack.

Figure 3. Instantiate the DBC-enabled stack. Click on thumbnail to view full-size image.

Figure 3 illustrates the following sequence of steps:

  1. The primary object StackImpl instantiates
  2. The primary object passes to method DBC.newInstance(), which instantiates DBC (the invocation handler)
  3. Using naming conventions and reflection, the invocation handler DBC finds, instantiates, and stores all StackImpl's assertion classes in the DBC instance
  4. DBC checks the invariants
  5. A dynamic proxy object, with the DBC instance as its invocation handler, instantiates and returns to the client

Note: StackImpl does not specify any new assertions. However, StackImpl does inherit the assertions defined in Stack and as a result must adhere to them. Figure 3 (and later Figure 4) illustrates the inheritance nature of the defined assertions.

Now that a DBC-enabled Stack instance exists, we can test its contractual obligations. For example, to see what happens when a Stack client violates a precondition assertion, let's call method stack.item() before any object is put on Stack. Here is the client code:

Object obj = stack.item();

Clearly the above statement violates the precondition at line 23 in Listing 1. Line 23 implies that an item from stack cannot be retrieved if the stack is empty. Therefore the following error message displays when the code executes:

Precondition violated: !obj().empty() at dbc.test.StackImpl.item

Clearly, the client caused this violation; it should have known not to retrieve an item from an empty stack.

Postconditions can also be violated. Remember these are the supplier's responsibility; in this example, StackImpl is the supplier. Therefore, to violate a postcondition, you must change the StackImpl implementation by removing or commenting out the put() implementation at line 41 in Listing 2. Then, if a client executes the following code segment that uses put():

stack.put("Any Object");

then StackImpl will violate at least two postconditions declared at lines 43 and 44 in Listing 1. Had the stack been empty prior to the put() call, the postcondition at line 42 in Listing 1 would be violated too. Here is the error message that would report:

Postcondition violated: !obj().empty() at dbc.test.StackImpl.put

The supplier is also responsible for the class invariant. To see what happens when an invariant is violated, you could change line 30 in Listing 2 to count() == 1. This will violate the invariant declared at line 6 in Listing 1. When the following client code executes:

Stack stack = (Stack)DBC.newInstance(new StackImpl(10));

the following error message will indicate a violated invariant:

Invariant violated: obj().empty() == (obj.count() == 0) at
dbc.test.StackImpl

As mentioned earlier, no assertions were declared in StackImpl, but the assertions declared in Stack were still enforced. This is an important concept to understand. A descendent cannot break the contract established by its ascendants. However, that does not mean StackImpl cannot have its own assertions; it may, as long as it fulfills the contract specified by Stack.

Figure 4 illustrates steps that occur when the client calls method put().

Figure 4. Method put() is called. Click on thumbnail to view full-size image.

Figure 4 illustrates the following sequence of steps:

  1. The client calls stack.put().
  2. A dynamic proxy intercepts the call and delegates it to DBC.invoke(). Method DBC.invoke() is the dynamic proxy's invocation handler.
  3. DBC.invoke() checks the preconditions with the help of naming conventions and Java reflection.
  4. DBC.invoke() calls the primary object (StackImpl.put).
  5. DBC.invoke() checks the postconditions with the help of naming conventions and Java reflection.
  6. DBC.invoke() checks the invariants.

Separate objects

As described earlier, preconditions have an optional wait statement, which is used in Listing 1 at line 23 and line 41 for methods item() and put(), respectively. This wait statement has no effect under regular circumstances; line 23 and 41 act as regular preconditions. For example, if the client calls method item() when the stack is empty, a precondition violation will occur, as the precondition states. If, however, we create the stack object as a separate object, the preconditions at line 23 and 41 would turn into wait statements. These statements will wait until the precondition is true.

The wait statements become relevant in situations of concurrent access. For instance, suppose the stack object is accessed from multiple threads. Suppose one thread reads (stack.item()), while another thread writes (stack.put(Object)). In Java, to achieve thread safety, you use the keyword synchronized. In the Stack example, we would implement the method item() as follows:

public synchronized Object item()
{
  while (!empty())
  wait()
  return 'top object';
}

Notice that item() caller(s) must wait until the stack isn't empty, as the precondition for item() states. To use preconditions as wait statements, you must be able to tell the DBC runtime environment that multiple threads will use the object. The DBC runtime environment needs to know that the object is separate. You can show that when you construct the object. The class dbc.DBC has an additional method: public static Object newInstance(Object obj, boolean separate). If the second argument separate in the newInstance() method above is true, the DBC runtime will do the following:

  • Protect each method on the primary object by using Java's keyword synchronized
  • Every precondition that has a wait statement after the Boolean statement will wait until that precondition is true (in the worst case, it waits forever)
  • Since we use some preconditions as wait statements rather than to check correctness, they are always turned on, and cannot be turned off

By adding the wait statement to the appropriate preconditions, you create a stack that you can use in a single-threaded environment as well as a multithreaded environment. Because preconditions are part of the stack specification and not its implementation, any dbc.test.Stack implementation will work in a single-threaded environment as well as a multithreaded environment.

Final thoughts: Importance of interfaces

In this article, we utilized DBC in Java by leveraging the following existing Java technologies:

  • Dynamic proxies and reflection for the DBC runtime environment
  • Javadoc and its Doclet API to generate assertion classes

Hopefully, this article has inspired you to try DBC, or at least think of developing software in terms of contracts between clients and suppliers. A DBC way of thinking is a great tool for developing high quality software. Additionally, DBC may even enhance your object-oriented analysis and design skills.

You can use the DBCProxy framework not only during design and development phases, but also during testing phases. The overhead of using the framework is minor. However, I don't recommend that you use the framework in a production system. The DBCProxy framework intends to aid in developing high quality software. It uses the Proxy pattern, while leveraging JDK 1.3's new dynamic proxy feature, as a means to execute assertions.

As I mentioned earlier, the DBCProxy framework will only work when objects are referenced within the interfaces they implement. This means that every DBC-enabled object must be based on at least one interface, and only methods that have been declared in an interface can be DBC-enabled. This is a consequence and limitation of dynamic proxies. However, it might not be a serious limitation since, in general, using interfaces in code is considered good design.

Anders Eliasson has more than 12 years' experience in software development and has been programming in Java since 1995. Anders is a Java 2 Certified Programmer and Java 2 Certified Developer. An event that solidified the role of DBC in his software development was a one-week object-oriented course with Bertrand Meyer. Anders would like to thank Charlie Hunt for his help reviewing this article.

Learn more about this topic

1 2 Page 2
Page 2 of 2