20. Abuse of Class Hierarchy

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

 

An Aside?

A student approached me offering code sort of like this:

class Basic {
    void someMethod() {
        if (this instanceof SubClassOne) {
            SubclassOne thisOne = (SubclassOne)this;
            operateOn( thisOne.somefield );
        } else {
            SubclassTwo thisTwo = (SubclassTwo)this;
            fiddleWith( thisTwo.otherfield );
        }
    }
}

class SubClassOne extends Basic {
    SomeClass somefield;
}

class SubClassTwo extends Basic {
    OtherClass otherfield;
}

The student was having problems with this. My observation was that, of course this code could be made to work, but this code is also significantly inefficient and it abuses the spirit of object oriented programming with class hierarchies.

Also, the above code is historically importat. Before the invention of objects, programmers in languages such as C and Pascal would write code like this in order to handle the kinds of problems that motivated the development of object-oriented languages.

Object-oriented languages allow us to replace this with something better better:

abstract class Basic {
    abstract void someMethod();
        // a concrete class could be used here too
}

class SubClassOne extends Basic {
    SomeClass somefield;
    void someMethod() { // overrides the basic version of SomeMethod
        operateOn( thisOne.somefield );
    }
}

class SubClassTwo extends Basic {
    OtherClass otherfield;
    void someMethod() { // overrides the basic version of SomeMethod
        fiddleWith( thisTwo.otherfield );
    }
}

Why is this better?

This puts all the code that deals with fields of each subclass in that subclass instead of making the superclass aware of those fields. It might even allow us to make the fields of the subclass private where the original scheme forces the fields to be public or as good as public.

It is worth noting that this is not always the most sensible way to do things. Specifically, if we had a factory method that allocated either an instance of SubclassOne or SubclassTwo the sensible place for this factory method will likely be in class Basic, since it always returns an instance of a subclass of Basic. If part of the initialization of the subclass instance by the factory method is specific to the type of that instance, it could be done in a method of the instance, but if that method simply sets a public field of the new instance, it would be just as easy to set that directly in the factory method.

Breaking up the Source File

The code for our road network simulator is getting large. At some point, it would make good sense to break it into multiple source files. The Java compiler allows you to say:

[HawkId ~]$ javac File1.java File2.java

This builds a project where the source is written in multiple files. However, if you had to type all the file names every time you compiled your program, it would get very tiring. Fortunately, the Unix/Linux shells (as well as the DOS/Windows command line) provide an abbreviation in the form of a wild-card character that matches essentially anything, so you can type:

[HawkId ~]$ javac *.java

This matches any file ending with .java, and it works well for a project where there is only one application in the directory.

The java compiler is fairly smart. If you type just this, the compiler will seek out other files in an intelligenty way:

[HawkId ~]$ javac RoadNetwork.java

Specifically, if the code in RoadNetwork.java refers to a class, for example, Simulation and that class is not defined in the current source file, the compiler will automatically look for a file named Simulation.java in the current directory and, if there is such a file, add it to the list of files it is compiling. It will not search outside the current directory.

If your project includes files that are in multiple directories, you can explicitly tell the compiler where they are using an @argfile. This tool allows you to have multiple main programs for a project, for example, main programs for testing classes in addition to the real main program, or code for shared components in other directories. An @argfile is a file with explicit lists of the source files to be used.

Suppose you wanted to call your argfile classes. You could construct such a file as follows:

Simulator.java
RoadNetwork.java

Given that this file exists, you can compile these two source files together with the command:

[HawkId ~]$ javac @classes

If the classes file gets big, it would be nice to be able to add comments to it. Unfortunately, Java does not have a standard way to put comments in @argfiles.

Breaking Out One Class

In the above example, we assumed that the classes file specified a separate source file for class Simulator. We can break this out of the original monolithic source file like this:

// Simulator.java

import java.util.PriorityQueue;

/**
 * Framework for discrete event simulation.
 * @author Douglas Jones
 * @version ...
 */
class Simulator {

    public interface Action {
	// actions contain the specific code of each event
	void trigger( float time );
    }

	... remainder of body of class ...
}

Note that our original source file had imported a large number of different classes. Of those, only class PriorityQueue is relevant to class Simulator, so that is the only import statement used in this source file.

Note also that the header comments from class Simulator have been expanded to include authorship and other information. When a source file is broken into multiple files, the relationship between those files needs documentation, and the Javadoc comment mechanism is reasonably good for this. Each source file is worthy of comments indicating authorship, version and the provenance of this file.

Class Simulator is almost the perfect size for a single class that fits comfortably in one source file. Some of our other classes are rather small, while others are a bit large.

The Remainder

Jave works nicely if all the classes you use are defined in one source file, and it works nicely if each class is in its own source file, so long as the source file has the same name as the class. Breaking up programs along other lines is more difficult. The problem is that if a class is needed, and the definition of that class is in a file with a different name, the compiler will not be able to find the file. It is only smart enough to look for a file with the same name as the class.

As a result, the "right" way to break up a large project into multiple source files puts one class in each file.

As a general rule, if the single-source file had a number of import statements at its head, when you break the program into multiple source files, each of those files is likely to have a much smaller number of import statements. You should never import files you don't need, since the import statements themselves serve as useful documentation of the relationship between the program and the tools in the Java library.

Access Rights

When a program is broken into multiple source files, there must be exactly one public class in each source file. That is the only class that can be directly accessed from the outside world. Other classes in the same source file can be declared with default access, which makes them accessible to all of the other classes in the same source file but not outside that file.

Only one public class in the program can have a public method called main. The whole idea of a public main method is something Java inherited from C++ and ultimately C.

Up to this point, we have not really distinguished between Java's default access rights, what you get when you say int i and public access, as in public int i.

This is because, so long as all the classes in a program are in the same source file, fields defined with default access are identical to fields with public access. When the file is broken into multiple files, however, things change. Fields declared as public are visible from code compiled in other source files, while fields with default access are only visible in the same source file where they are declared.

This relates strongly to features of C++ and C where source files were a primary access control mechanism. With inner classes, Java could have solved these kinds of problems quite differently, but inner classes are an afterthought in the history of Java, and to this day, access rights controls on inner classes are only sloppily implemented in the language.

Javadoc

When everything was in one source file, you could just do a text search to find something. Internal comments were useful, but we primarily used Javadoc comments as if they were internal, without fully exploiting them.

When your program gets big, Javadoc becomes far more useful. If you've used Javadoc comments in your code, you can run Javadoc over the program with one of the following shell commands -- the shell parameters should be the same ones you use with the Java compiler when you compile your code:

javadoc Classname.java
javadoc *.java
javadoc @classes

This will create a web site of documentation for your program, one web page per class. If you open a web browser on the index.html page this creates in your directory, you can dive into this web site and explore the kind of documentation Javadoc creates from the Javadoc comments in your code.

Very likely, what you'll find is that your Javadoc comments weren't very good. Once you start using Javadoc, you really should wander the web site it generates and do a critical reading of the content of that web site. One thing you'll notice immediately is that almost all of the huge web site documenting the Java library was generated using Javadoc. That is, the library documentation was all derived from extensive comments in the actual Java source code for that library.

Literate Programming

Javadoc, or rather, the style of commenting that Javadoc uses, is an example of a broad class of techniques described as literate programming. The old, low-tech approach to programming is to develop code and documentation as entirely separate documents. You write the program here, and you create a web page or other document describing the program there, using unrelated tools and unrelated languages. Perhaps you write the program in C++ and then write the manual using Microsoft Word.

In 1984, Donal Knuth published a book, Literate Programming, that encourages a different approach. In Knuth's model, the programmer creates a single document that incorporates program fragments woven into the documentation for that program. Knuth provided two tools for processing this document. One tool traverses the document, weaving together the program fragments into a program in the target programming language, while the other tool traverses the document, typesetting human readable text to document that program.

Knuth's tools for supporting literate programming were Web (for Pascal programs) and later Cweb (for C programs). Noweb was derived from this as a language-independent literate programming environment, and many others have followed in this direction.

Javadoc is not quite the same as Knuth's idea, but it is in the same broad category. Unlike Knuth's idea, Javadoc does not allow the order or structure of the Java program to be assembled "out of order" or rather, in the order that makes sense to the programmer, as opposed to the language. It does, however, support the idea of creating a single document from which documentation and code can be derived.

A Javadoc Example

Let's look at a simplified small example class from our highway network simulator:

class MyRandom extends Random {
    private MyRandom() {
        // uncomment exactly one of the following!
        super();                // let Java pick a random seed
        // super( 3004 );       // set seed so we can debug
    }

    static final MyRandom stream = new MyRandom();

    public static MyRandom stream() {
        return stream;
    }

    public double nextExponential( double mean ) {
        return -Math.log( nextDouble() ) * mean;
    }
}

The above code is from the version of the road-network simulator from before it was broken into multiple source files and with most of the old comments stripped out. There are numerous places in this code where we can put Javadoc comments as we prepare it to be a stannd-alone source file as part of a multi-file project:

import java.util.Random; /** * Singleton wrapper for Java's Random class * @author Douglas Jones * @version 10/27/2020 * Status: Fairly stable, but other probability distributions could be added * @see java.util.Random */ public class MyRandom extends Random { private MyRandom() { // uncomment exactly one of the following! super(); // let Java pick a random seed // super( 3004 ); // set seed so we can debug } /** the only stream visible to users * users can use the <TT>stream</TT> field or * the <TT>stream()</TT> method interchangably. */ public static final MyRandom stream = new MyRandom(); /** an alternate way to expose users to the stream * @return handle on the stream * users can use the <TT>stream</TT> field or * the <TT>stream()</TT> method interchangably. */ public static MyRandom stream() { return stream; } /** get the next exponentially distributed pseudo-random number * @param the mean value of the distribution * @return the next number drawn from this distribution */ public double nextExponential( double mean ) { return -Math.log( nextDouble() ) * mean; } }

The documentation output of Javadoc will use this text to build the global structure of the document, but each public component of the class should also be documented. Note that all kinds of HTML may be included in Javadoc comments. The <p> tags used above are paragraph dividers, while the <pre> and </pre> tags are used to set out pre-formatted segments of code.

Using Javadoc

Once you have a file that contains Javadoc comments, you can comple it with the Java compiler using the javac command, and you can create an HTML (world wide web) document describing the contents of that file using the javadoc command. For example, you could type:

[HawkId ~]$ javadoc ScanSupport.java

If you do this, it will output any error messages resulting from misformed Javadoc comments, and then it will create a flood of interlinked .html files, as well as a javascript file (.js suffix) and a style sheet (.css suffix).

You can do this one file at a time when you are developing your code, but the real power of Javadoc comes to the fore when you use Javadoc on the main class of your project or on the set of all .java files that make up your project. When you do that, the set of files generated by Javadoc becomes a complete web site describing your project.

In fact, if you use a web browser to view the output of Javadoc, what you will see is documentation that, at least in style, resembles the on-line documentation provided by Oracle for the Java libraries. That is because Oracle uses Javadoc to prepare this documentation as they maintain their libraries.