8. Errors, Organization, and Scaffolding

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

 

Where were we?

Try to compile our code and we get an error message!

[HawkID@serv16 ~/project]$ javac Road*java
RoadNetwork.java:92: error: variable sc might not have been initialized
                while (sc.hasNext()) {
                       ^
1 error
[HawkID@serv16 ~/project]$ vi Road*java

What's the problem? Here's the main method we have at this point:

        public static void main(String[] args) {
                // open the input file from args[0]
                // Bug:  Did the user provide args[0]?
                Scanner sc;
                try {
                        sc = new Scanner( new File( args[1] ));
                } catch (FileNotFoundException e) {
                        // Bug:  What to do here?
                }

/* !!! */       while (sc.hasNext()) {
                        // until the input file is finished
                        String command = sc.next();
                        if ((command == "intersection")
                        ||  (command == "i")) {
                                // Bug: Something about an intersection
                                inters.add( new Intersection( sc, inters ) );
                        } else if ((command == "road")
                        ||         (command == "r" )) {
                                // Bug: Something about a road
                                roads.add( new Road( sc, inters ) );
                        } else {
                                // Bug: Unknown command
                        }
                }
        }

The compiler is complaining about the line marked /* !!! */ The problem is, we haven't decided what to do about error reporting, and even if we did make a decision, the compiler might not be smart enough to figure out that the actions we take in response to a file not found exception guarantee that the input scanner sc is properly defined.

Errors

There are some obvious ways to deal with errors! We could throw an exception, for example, and leave it to the default handler to deal with the problem. This is not a great idea unless you also include an exception handler to deal with the error.

An 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 open classes, for example.

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 );
                }
                if (args.length > 1) {
                        System.err.println( "Unexpected extra arguments" );
                        System.exit( 1 );
                }
                ...

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 system 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 is verbose, but is has a second problem. What if we want to add a GUI interface to this application? The problem with GUIs is, 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.

So, what we must do is provide a centralized error handler, for example, consider:

class Errors {
        /** Error reporting framework
         */
        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 a popup window, reporting the error, and then waiting for the user acknowledgement. Later, we might also add non-fatal error dialogues, possibly also managed through this class. Our main method would now begin like this:

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

The Uninitialized Variable

The above code does not solve our problem with an uninitialized variable. We could, of course, move the entire input processing loop into the try block:

        public static void main(String[] args) {
                // open the input file from args[0]
                if (args.length < 1) {
                        Errors.fatal( "Missing filename argument" );
                }
                if (args.length > 1) {
                        Errors.fatal( "Unexpected extra arguments" );
                }
                try {
                        sc = new Scanner( new File( args[1] ));
                        while (sc.hasNext()) {
                                // until the input file is finished
                                String command = sc.next();
                                if ((command == "intersection")
                                ||  (command == "i")) {
                                        // Bug: Something about an intersection
                                        inters.add(
                                                new Intersection( sc, inters )
                                        );
                                } else if ((command == "road")
                                ||         (command == "r" )) {
                                        // Bug: Something about a road
                                        roads.add( new Road( sc, inters ) );
                                } else {
                                        // Bug: Unknown command
                                }
                        }
                } catch (FileNotFoundException e) {
                        Errors.fatal( "Can't open file '" + args[1] + "'" );
                }

This gets very ugly very quickly! Already, one of the lines crept over the 80-column line length, causing an ugly wrapped line in our display. We could, of course, simply widen our display window, but that doesn't solve the basic problem that the entire method is on the verge of growing too big to manage. Try-catch blocks that are longer than a few lines get very hard to follow.

In the long run, we will be better off splitting the code for reading the road network into a second method of the class RoadNetwork. Consider this:

        private static void readNetwork( Scanner sc ) {
                while (sc.hasNext()) {
                        // until the input file is finished
                        String command = sc.next();
                        if ((command == "intersection")
                        ||  (command == "i")) {
                                // Bug: Something about an intersection
                                inters.add( new Intersection( sc, inters ) );
                        } else if ((command == "road")
                        ||         (command == "r" )) {
                                // Bug: Something about a road
                                roads.add( new Road( sc, inters ) );
                        } else {
                                Errors.fatal(
                                        "'"
                                        + command
                                        + "' not a road or intersection"
                                );
                        }
                }
        }

Here, in addition to moving the code for picking apart the input file into a new method called readNetwork(), we fixed one bug, adding a call to our new Errors.fatal() method to handle the case of a line of text that didn't include one of the permitted keywords.

Note that, once we start defining really big road networks, it might be handy to extend the set of keywords to permit comments of some kind in the road network description file. Perhaps lines starting with a "-" as their first nonblank could be comment lines? We can defer this question for now, but the problem is easy to solve when the time comes.

Given that we've created the method given above, we can modify our main method as follows:

        public static void main(String[] args) {
                // open the input file from args[0]
                if (args.length < 1) {
                        Errors.fatal( "Missing filename argument" );
                }
                if (args.length > 1) {
                        Errors.fatal( "Extra command-line arguments" );
                }
                try {
                        readNetwork( new Scanner( new File( args[1] )));
                } catch (FileNotFoundException e) {
                        Errors.fatal( "Can't open file '" + args[1] + "'" );
                }
        }

Notice that we no-longer have a local variable sc in the main method because it was never needed anywhere but in readNetwork(). As a result, we merely pass the newly allocated and initialized scanner to readNetwork().

When we tested the code, we got a run-time error because args[1] was undefined. This is a run-time error, not a compile-time error, and looking back at the code, you can see that it is a typical example of a really stupid error. The comment at the head of the main method says (correctly) open the input file from args[0]. Obviously, we knew what we were doing when we wrote that comment, and then promptly forgot it when we wrote the code to open the input file and to complain about input files that can't be opened. The fix is trivial, change args[1] to args[0].

Scaffolding

Before we continue, suppose we completely initialized the data structure representing a road network. Aside from not outputting any error messages, how would we know that our code worked? Given what we have so far, the answer is, we wouldn't. That makes debugging difficult, so we need to add something to allow us to examine the road network we have built.

This is a feature common to large programming projects. If our goal is to build a simulator for a highway network, we never need to print out that network. The code we develop here to print the network counts as scaffolding, material used to help build the final product, but then discarded when the final product is complete. However, In the case of buildings, scaffolding is typically torn down after the building is complete, and then, if the building needs repairs, we put up new scaffolding. In the case of software, it makes sense to save the scaffolding so that we can re-use it to test any changes made to the program.

So, here is heart of the main method with a one more bug corrected (the problem with args[1] fixed) and a call to a new scaffolding method added:

                try {
                        readNetwork( new Scanner( new File( args[0] )));
                        writeNetwork();
			/* Bug:  above call replaces actual application */
                } catch (FileNotFoundException e) {
                        Errors.fatal( "Can't open file '" + args[0] + "'" );
                }

The writeNetwork() method will allow us to evaluate whether the readNetwork() method actually did its job. We followed the call with a bug notice serving as a reminder that this call is scaffolding that will eventually be removed and replaced with something useful.

There are a number of design decisions to be made in designing the writeNetwork() method. The following code suggests the biggest of these decisions by treating roads and intersections quite differently:

        private static void writeNetwork() {
                for (Intersection i: inters) {
                        i.printMe();
                }
                for (Road r: roads) {
                        System.out.println(
                                "road "
                                + r.sourceName
                                + " "
                                + r.destName
                                + " "
                                + r.delay
                        )
                }
        }

The for loops above implicitly create an iterator over the list and then use that iterator to get successive list elements. We could have written a far more long-winded form for either loop, for example:

                for (
                        Iterator <Intersection> it = inters.iterator();
                        it.hasNext();
                ) {
                        Intersection i = it.next();
                        i.printMe();
                }

The above long-winded code expresses exactly the same computation, and in fact, the short-hand version creates exactly the same for-loop. Any for loop can be rewritten as a while loop, so we could even rewrite this code as follows:

                {
                        Iterator <Intersection> it = inters.iterator();
                        while ( it.hasNext() ) {
                                Intersection i = it.next();
                                i.printMe();
                        }
                }

Our focus here is on the question of how to write the bodies of each of the for loops. We could add a method to the intersection and road classes so that each class instance knows how to print itself, or we could reach into the class instance and pull out the pieces we need in order to print that object.

It is clear that both approaches can be made to work. Reaching into the object in order to pick out fields is, however, dangerous. The author of the object might well decide to change its internal organization. For example, after it is initialized, why should a road remember the textual names of the intersections to which it connects? Why not just keep pointers to (handles of, references to) the intersections, and let each intersection keep track of its own name.

If that design change is made and all details of how to print a road are part of the Road class, then changes to the representation of a road are all internal to that class. If this is not done, changes to the road class must be followed with changes scattered throughout the rest of the program. This leads us to prefer the idea of having a local method in every class for printing that class.

Above, we called this the printMe() method, but note that all Java classes have a standard method called toString(). Thus, we could also write this:

        private static void writeNetwork() {
                for (Intersection i: inters) {
                        i.printMe();
                }
                for (Road r: roads) {
                        System.out.println( r.toString() );
                }
        }

The nice thing about using the toString() alternative is that it leaves the question of where the output is directed completely local to writeNetwork. That means that, if later, we want to create a version of writeNetwork that directs its output elsewhere, we can do this with local changes instead of having to reach deep into the other methods of other classes to make this change.