39. Eliminating Circularity, Event-Drive Code

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

 

A Final Note on Eliminating Circular Dependencies

In the last lecture, we got this far:

public interface Road {
    public void enter( double time, Vehicle v );
}

public interface Intersection {
    void setIncoming( Road r );
    void arrive( double time, Vehicle v, int dir );
}

public interface Vehicle {
    Road pickOutgoing( Intersection i, ArrayList<Road> r );
}

We were left with what looks like an unbreakble knot of interdependencies. In fact, it is not unbreakable, but there is a small cost to breaking it. The key to breaking it is to use Java's class hierarchy. All user-defined classes in Java, if they do not explicitly extend a pre-existing class, are extensions to class Class. It is class Class that provides things like the default toString() method of every class that does not override this method.

Aside on Abstract Classes

We didn't need to use interfaces here. We could also have done the same thing with abstract classes.

public abstract class Road {
    public abstract void enter( double time, Vehicle v );
}

public abstract class Intersection {
    public abstract void setIncoming( Road r );
    public abstract void arrive( double time, Vehicle v, int dir );
}

public abstrat class Vehicle {
    public abstract Road pickOutgoing( Intersection i, ArrayList<Road> r );
}

So why does Java provide two ways to do the same thing? The answer is, we are dealing with design by afterthought. Interfaces were added later in the history of Java to solve problems that the original Java didn't solve. It is completely fair to describe Interfaces as a kluge added to the design of Java after the discovery that the Java class hierarchy could not be fixed to do certain things. The problems that Java interfaces solve have nothing to do with the code here.

Breaking the Dependencies

What we do to break the circular dependencies above is introduce just enough use of class Object to break the circularity:

public interface Road {
    /** @param ov the Vehicle that enters the road */
    public void enter( double time, Object ov );
}

public interface Intersection {
    void setIncoming( Road r );
    /** @param ov the Vehicle that arrives at this intersection */
    void arrive( double time, Class ov, int dir );
}

public interface Vehicle {
    Road pickOutgoing( Intersection i, ArrayList<Road> r );
}

In the above, we deliberately broke all the dependencies that went down the page, while leaving the ones that went up the page alone. Regardless of the order we list the classes, this approach always breaks the cycles, although it breaks different dependency links depending on the order.

In the original code, with circular dependencies, we could rely on the Java compiler to check the classes of each object passed as a parameter. Now, we have lost that ability. Instead, we moved the actual classes needed into the Javadoc comments for each method.

This change does not require any changes to the code that calls the methods we changed. It is always legal to pass a Vehicle as an actual parameter where the formal parameter is declared as an Object. We do have to change the method implementations in order to receive an Object as a parameter and use it as a Vehicle. We do this with casting:

public class RoadImp implements {
    /** @param cv the Vehicle that enters the road */
    public void enter( double time, Object ov ) {
        Vehicle v = (Vehicle)ov;
        ... the rest of the code ...
    };
}

Of course, we should also change the makefile to document what we've done!

This change has a small run-time cost: As we have noted previously, in order to make Java strongly typed, casting in Java implies checking the class membership of the object. Each time the enter method is called, it must check to see that the object passed was really a Vehicle and not something else.

We moved the responsibility for some of the type checking in our program from the compiler to the run-time system, but we got rid of all of the circular dependencies in our Road-Network simulator. Was it worth it? Not really, but the exercise does teach us something about the language and about the hierarchic structure of programs.

Other EventDriven Frameworks

Event-driven frameworks are very common in modern computing:

Window managers are typically written with mouse events, keypress events, and so on, so applications are written as collections of methods that are called when these events occur. The top level structure of many GUI based applications looks like this:

[ first initialize everything ]
while (true) {
    WindowEvent e = gui.nextEvent();
    switch (e.type) {
	case mouseMove: e.widget().mouseMove( e.mouseCoords() ); break;
	case mouseClick: e.widget().mousePress( e.mouseButton() ); break;
	case mouseClick: e.widget().mouseRelease( e.mouseButton() ); break;
	case keyPress: e.widget().keyPress( e.key() ); break;
	case keyRelease: e.widget().keyRelease( e.key() ); break;
    }
}

In the above, the GUI software uses its model of screen focus to determine which widget on the screen to deliver the event to. Windows, push buttons, text-editing boxes and similar things are all subclasses of widget. The event notice includes what widget the event occurs in, and what happened in that widget, a mouse move, mouse button click, key press, or key release. The main loop calls the appropriate method of the widget for the particular kind of event, and passes that method the details of the event such as where was the mouse, what mouse button was pressed, or what key on the keyboard was involved. It is up to the programmer to write the event service routines in each widget to make the widget behave in a coherent way in response to the sequence of events reported to it.

Transaction processing servers are typically written with an event-driven structure. When you fill out a web form, you are working locally on your client machine until you hit the submit button. At that point, the completed form is delivered, as a single lump, with a "form completed" event. There is typically one event handler for each type of form that a user might fill out.

The notion of a particular client process state is created by variables maintained on the server or, in some cases, by cookies stored on the client's machine and submitted to the server with the form. The logical process, from the client perspective, is the sequence of forms that the user fills out as they visit a the web site managed by that server.