User interfaces for object-oriented systems, Part 5: Useful stuff

Build an application that puts user-interface principles into practice

No theory is worth anything if it can't be applied effectively, so this month and next, I'll build a small but nontrivial application that demonstrates the object-oriented user-interface principles I've covered over the last few months: an RPN (Reverse Polish Notation) calculator that can work as either a tape calculator or a keypad calculator. (One of the main things I want to demonstrate is the flexibility of the UI; thus the two presentations.) Since this application is a real one, I need to do some groundwork before covering the calculator itself.

Consequently, this month's Java Toolbox takes the name of the column literally and presents the set of generic tools we'll need to build the calculator -- little tools of general utility that should be of use for more than the current application. Specifically, I'll cover the following:

Assert An implementation of an "assertion" that lets you implement pre- and postconditions.
Tester A class that aids in putting together automated unit tests.
Bit_bucket The Java equivalent of /dev/null, it throws away characters.
Std A "singleton" wrapper around standard input and output that makes it easy to access these two streams.
Log A convenient class for logging diagnostic messages.
Align A utility containing various methods for aligning text.
Scrollable_text_area A JTextArea that has scroll bars around it.

Most of those classes are unrelated to one another, and none of them is significant enough to merit a article devoted exclusively to it, so lumping them all together in one place seems sensible. We've taken a quick look at a few of the tools in the past, but a review should prove helpful. Now, on to the tools.

TEXTBOX: TEXTBOX_HEAD: Build user interfaces for object-oriented systems: Read the whole series!

:END_TEXTBOX

Assertions

Bertrand Meyer, the author of the Eiffel language, came up with the notion of pre- and postconditions, commonly called assertions. Eiffel, described in Object-Oriented Software Construction (see Resources) supports those concepts directly within the syntax of the language. Java, unfortunately, provides no such support.

The basic idea of a precondition or postcondition is that every method expects the world (the object that it's working on, the global state of the program, the arguments to the method, and so on) to be in a particular state when the method is called, and that every method will leave the world in a particular state. An assertion is a body of code that guarantees that those conditions hold and typically terminates the program with an exception toss if they do not. Assertions are typically implemented as debug-time tests and removed from the code entirely when the code goes into production. (The removal can be risky since you are modifying tested code, but on the other hand, the assertions can slow down the execution of a program noticeably.)

I've implemented assertions using the two classes defined in Lists 1 and 2. The first version, in the com.holub.tools package, is made up of empty methods. Import this version into your code when you've finished debugging. The second version, in the com.holub.tools.debug package (List 2), actually does something. Import this version when you're still working on the code.

When you use the class like this:

Assert.is_true( condition );

the program terminates by throwing an Assert.Failed object if the condition is not true. A second version lets you add a reasonable error message to the thrown object:

Assert.is_true( condition, "Error that caused the problem" );

I've also provided is_false() variants as a programming convenience.

The main problem with my strategy of using an empty method to eliminate the overhead of the assertion is that the resulting efficiency is somewhat JVM dependent. Hotspot, for example, does everything right. It notices that the method is empty, so expands it inline, effectively removing the call from the .class file. Hotspot even removes the argument to the assertion (effectively dead code). The last behavior can be important if you use string concatenation in an argument to is_true() or its equivalent. You don't want to go through the considerable overhead of building a string with a series of concatenations if the string isn't used for anything. Earlier JVMs were not as good about this optimization: they would correctly eliminate the call itself but typically wouldn't eliminate the argument evaluation.

Finally, note that method calls in assertions are risky. Hotspot will evidently inline them up to a point, but it won't chase down the call graph indefinitely. Generally, method calls should appear in assertions only if they do nothing other than return constant values. If you want to test a method's return value, call the method first, then test the resulting value in a subsequent assertion.

Of course, methods that have side effects, which modify the state of the program, should never be invoked from within an assertion because they'll go away when you move from the debugging to the production version of the code.

Let's look at List 1:

Next, we take a look at List 2:

List 2. /src/com/holub/tools/debug/Assert.java
   1: package com.holub.tools.debug;
   2: 
   3: public class Assert
   4: {
   5:   public final static void is_true( boolean expression )
   6:     {   if( !expression )
   7:             throw( new Assert.Failed() );   
   8:     }
   9: 
  10:   public final static void is_false( boolean expression )
  11:     {   if( expression )
  12:             throw( new Assert.Failed() );   
  13:     }
  14: 
  15:   public final static void is_true( boolean expression, String message)
  16:     {   if( !expression )
  17:             throw( new Assert.Failed(message) );    
  18:     }
  19: 
  20:   public final static void is_false( boolean expression, String message)
  21:     {   if( expression )
  22:             throw( new Assert.Failed(message) );    
  23:     }
  24: 
  25:   static public class Failed extends RuntimeException
  26:   {   public Failed(            ){ super("Assert Failed"); }
  27:       public Failed( String msg ){ super(msg);             }
  28:     }
  29: }   
         

Help in unit testing

The next helper class of interest is the Tester, which makes it easier to do unit testing (the standalone testing of a single class).

Test shows a typical use of this class. I typically put my unit tests in a Test inner class of the class I'm testing. (I use an inner class so that the code that makes up the test won't be part of the class file that represents the class I'm testing. Inner classes create their own class files, which in this case do not have to be shipped with the production code.)

I have several criteria for what constitutes proper test behavior. A formal test must:

  1. Not print anything unless something is wrong
  2. Have a unique ID
  3. Test reasonable boundary conditions and "impossible" inputs

The first item is particularly important because, in an automated testing environment, you don't want to clutter up reports with worthless information. A test typically tries to find out when something goes wrong, so that's the only time that output should be generated. On the other hand, I occasionally do get paranoid, so I've provided a hook that modifies the behavior of my class to print both success and failure messages.

The tester is initialized with a normal constructor call that has passed two arguments: first, a Boolean flag that indicates whether the output is verbose (success messages are printed in addition to failures) or not (only failure messages are printed); second, a PrintWriter that represents the stream to which output is sent. Thereafter, you run a test by calling the check() method, which has several overloads.

The arguments are:

  • The test ID
  • The expected output of a method call
  • The method call to test

The overloads change the types of the second and third arguments to handle various possible return values. Supported return values are String, StringBuffer, double (which will handle float via the normal type conversion), long (which will also handle char, int, and so on, via the normal type conversions), and boolean. There is only one requirement: the second argument's type must be the same as the return type of the method call in the third argument.

The final method of interest in the Tester is the exit() method, which causes the program to exist with the current error count as its exit status. This mechanism lets you write shell scripts that run tests and detect when something goes wrong.

List 3. Using the Tester class
   1: private static class Test
   2: {   
   3:   public static void main(String[] args)
   4:     {
   5:     com.holub.tools.Tester t = 
   6:             new com.holub.tools.Tester( args.length > 0,
   7:                                 com.holub.io.Std.out());
   8:     //...
   9:     t.check("align.1", "01234", Align.left("01234", 5));
  10:     t.check("align.2", "     ", Align.left("",      5));
  11:     t.check("align.3", "X    ", Align.left("X",     5));
  12:     //...
  13: 
  14:     t.exit(); // exits with a status equal to the error count
  15:     }
  16: }
         

The code, which is largely self-explanatory, is in List 4:

1 2 3 4 5 Page 1