6. Errors, Organization, and Scaffolding

Part of CS:2820 Object Oriented Software Development Notes, Fall 2017
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

 

A Hierarchy of Virtual Machines

One approach to designing a large system is to view the system as a set of layers, where each layer is, in effect, a programming language or a programmable machine. Consider this view:

Hardware
The bottom layer is a areal machine of some kind.
Operating System
User applications run on a virtual machine built by layering an operating system on top of the hardware.
Programming Environment
Most applications don't use the bare operating system, they add a set of resources to the system resources that are created, for example, by the standard library of the programming language they are using. So, for example, we are using the Java library on top of the Linux operating system.
Special-purpose components
Large applicatoins typically include their own collections of purpose-built classes, that sit on top of the system and language, creating an environment in which the final development steps are simplified.
Higher-level components
There may be several layers of custome components sitting under a very large application.
Top level code
If the lower layers are well thought out, the top level code to implement the application can end up being very simple.

It is important to note that, in a layered system, test frameworks can be built for each layer, where the test framework does not depend in any way on the behavior of the higher layers (the layers closer to the end user and application). Furthermore, lower layers in the system may be repurposed to serve other applications.

Note that developing a hierarchy of virtual machines from the bottom up can be very messy because until the final application is developed, there is little motivation or direction for the lower layers. Nonetheless, this is a workable incremental programming methodology.

The ideas of extreme programming suggest that it might be better to slice the system diagonally, building a small part of the final application on top of a small part of each of the lower layers, growing each layer as needed to support the part of the top-level application that is next on the development list. The point here is, it can be constructive to view a system as made of multiple layers of virtual machines even if the layers are incompletely defined to begin with and even if the order of development is not the same as the order of the layers.

Transparency

In any system composed of layers of virtual machines, each layer can be transparent or opaque (or a mosaic of transparent and opaque parts).

Where a layer is transparent the behavior of the underlying layers is completely exposed. If a programmer working on the lower layer could do something, a programmer working on the upper layer can do the same thing. A transparent virtual machine adds features or functions without preventing use of the lower layers.

Where a layer is opaque it prevents certain behaviors that a programmer at the lower level could have evoked. Opaque layers can prevent unsafe activity.

This notion was originally developed by David Parnas, who illustrated it with the following example: Consider a vehicle with 4 wheels (like a car) where the front wheels can be independently turned to steer the vehicle. This vehicle is both very manuverable and very dangerous. If you steer the front wheels so that the lines of their axles intersect the line of the rear axle at a single point, you can turn the vehicle around the point of intersection. Because the front wheels can be turned arbitrarily, you can even turn the vehicle around the point midway between the rear wheels -- allowing the car to rotate in place and making it very easy to park your car.

On the other hand, if you are moving at any speed and you turn the front wheels so that the lines of their axles don't follow the basic rule above -- intersecting the line of the rear axles at two different points -- the front wheels will skid and you will lose control. At high speeds, this mistake can cause the vehicle to flip over, killing the driver.

In automobiles, we add an opaque virtual machine on top of the independently hinged front wheels. That layer consists of a (modified) parallelogram linkage that keeps the front wheels approximately parallel and, for wide radius turns, comes close to the ideal of keeping the lines of their axles intersecting the line of the rear axle at a single point. This virtual machine is not transparent -- it completely prevents turns below a minimum radius, so you have to learn complex manuvers for parallel parking, but it also prevents you from putting the front wheels into strange positions that would be very dangerous in a moving car.

The access control mechanisms of languages like Java are there to allow you to control the transparency of your implementations. If you declare components of your code to be private, for example, you can prevent unsafe manipulation of those components. Note, however, that the access control mechanisms of Java, while quite powerful, are not a complete set.

For example, if you declare a class with two methods and one component variable, you cannot declare that variable to be read-only when seen from one method and read-write when seen from another. People have proposed programming languages where this kind of fine grained control is possible, but the best you can do in Java is to add comments saying, for example, "here, variable x is never modified" where you might want to have the language prevent modification.

Where were we?

Try to compile our code and we get an error message! We declared several variables to be of class string when we should have said String, and we forgot to import FileNotFoundException. Also, we wrote the constructor for Road() but not Intersection(), and we need both of them.

Fixing these leaves us with a program that contains no compile-time errors, but it's not interesting and there is no way to test it. Our extreme-programming framework demands that every step in our development be testable. To do this, we need some immediate testable goal for our code, something far short of an executable road-network simulation.

Something Testable

The code we have reads a file describing a road network, so our initial test can be code that reads this file and then writes it out. If the program is correct, the output should resemble the input.

Resemble, not be exactly the same as. Why? Because the tools in class Scanner squeeze out extra spaces and because, depending on how the members of class LinkedList are handled, the order of roads and intersections may not be the same.

Every Java object has a toString() method that converts that object to a textual representation as a character string. The default method creates a near-useless representation, but we can easily override the default. For example, here is an appropriate toString() method for our class Road:

    public String toString() {
        return (
            "Road " +
            sourceName + " " +
            dstName + " " +
            travelTime
        );
    }

Assuming that we write similar code for class Intersection, the main program can be modified to call printNetwork() after it calls readNetwork(). The obvious thing to do is to make printNetwork() be a static method of RoadNetwork, just like readNetwork(). The following code should suffice for the toString() method for class Road:

    /** Print out the road network from the data structure
     */
    static void printNetwork() {
        for (Intersection i:inters) {
            System.out.println( i.toString() );
        }
        for (Road r:roads) {
            System.out.println( r.toString() );
        }
    }

Errors

There are some obvious ways to deal with errors! We could throw an exception, for example, but Java demands that you write handlers for exceptions, so this doesn't get us anywhere.

An easy alternative is to call System.exit( 1 ). This terminates the program rather violently. By convention, the integer parameter to exit() should be zero if the program is terminating normally, while it should be nonzero in case of abnormal termination. The exit is violent, in the sense that all kinds of things are abandoned -- finalization on objects is ignored, for example. The convention that the parameter should be zero for success and nonzero for failure is a standard inherited from Unix and Linux; it only really matters when programs are called from elaborate shell scripts or Perl scripts.

Of course, before calling exit(), we should output an error message! Consider the following rewrite of the start of the main method:

    public static void main(String[] args) {
        // open the input file from args[0]
        if (args.length < 1) {
            System.err.println( "Missing filename argument" );
            System.exit( 1 );
        } else if (args.length > 1) {
            System.err.println( "Unexpected extra arguments" );
            System.exit( 1 );
        } else ...

First note, instead of outputting error messages to System.out, the standard output stream, we have output the error messages to System.err. All processes on systems descended from UNIX have two default output streams, one for normal output, and the other for error messages. Linux and Windows both support this idea. System documentation usually refers to these streams as stdout and stderr, but in Java, they are called System.out and System.err.

This approach to dealing with errors leads us to rather verbose code, and it locks us in by distributing error reporting code all over the program. What if we want to add a GUI interface to this application? If we proceed as suggested above, we'll have to inspect the entire program to find all places where error messages are output and modify all of them to user our new GUI. With a GUI, we probably don't want to just output a message and then immediately kill the applicaton. If we did that, all the open windows attached to the application would close with a bang, including wherever the error message appeared. Instead, we want to open a popup window and have it hang there until the user has read it. We certainly don't want to duplicate all the code for that everywhere an error message is output.

What we need to do to avoid both of these problems is provide a centralized error handler, for example, something like this:

/** Error reporting framework
 */
class Errors {
    static void fatal( String: message ) {
        System.err.println( message );
        System.exit( 1 );
    }
}

Later, if someone wants to convert our program to use a GUI, this one method would be responsible for opening the popup window, reporting the error, and then waiting for the user acknowledgement. Later, we might also add non-fatal error dialogues, and this class is a natural place to put them. Our main method would now begin like this:

    public static void main(String[] args) {
        if (args.length < 1) {
            Errors.fatal( "Missing filename argument" );
        } else if (args.length > 1) {
            Errors.fatal( "Unexpected extra arguments" );
        } else try {
            readNetwork( new Scanner( new File( args[0] ) ) );
        } catch (FileNotFoundException e) {
            Errors.fatal( "Can't open file '" + args[0] + "'" );
        }
    }

When it comes time to report less serious errors, we could add a warning() method to class Errors that works much the same as the fatal() method except that it does not exit.

We can call this, for example, in readNetwork():

    private static void readNetwork( Scanner sc ) {
        while (sc.hasNext()) {
            // until the input file is finished
            String command = sc.next();
            if (command == "intersection") {
                inters.add( new Intersection( sc, inters ) );
            } else if (command == "road") {
                roads.add( new Road( sc, inters ) );
            } else {
                // Bug: Should probably support comments
                Errors.warning(
                    "'"
                    + command
                    + "' not a road or intersection"
                );
                // Bug: Should skip to the next line
            }
        }
    }

What we have done here is build a new layer on top of the virtual machine hierarchy supporting our program. This layer sits on top of the Java library and below our main program. It extends our programming environment by adding a uniform error handling mechanism. As we further develop our program, we will add other supporting mechanisms to this virtual machine.

Static methods

In Java, all subroutines are described as methods (the term subroutine being the most generic of all names for chunks of executable code). Java methods come in three flavors, regular methods, static methods and constructors.

Regular methods — true methods — must be called relative to an object. For example, System.out is the name of an object, an output stream, and println() is a method of the stream class. The call System.out.println() calls the println() method in the context of the object System.out. True methods cannot be called except in the context of a particular object.

Static methods such as Errors.fatal() do not have an object as their context. The only variables they can operate on are their parameters and static variables -- that is, variables that are not part of an object. Prior to the invention of object-oriented programming, the term method was not used. The term subroutine comes from FORTRAN in the 1950s. The term procedure comes from Algol. In both languages, a function is a kind of procedure or subroutine that returns a value. All of these can be translated to Java as static functions.

Java constructors are a special case. To a large extent, they are analogous to static methods except that they also operate on newly allocated object that they implicitly return, without the need for a return statement.