22. Changing the Simulation Framework

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

 

Introduction

We have created a road-network simulation and an epidemic simulation using one framework. The interesting question is, can we change the framework without destroying the other work we have done? The answer to this question is, to a large extent, yes. Adopting a simulation framework at the start of a project is important, but within reasonably wide bounds, a change of framework is not outrageously expensive.

A new Framework

The framework we used before allowed us to schedule the deferred execution of λ expressions. Here is an alternative framework, stripped of commentary:

import java.util.PriorityQueue;

public abstract class Simulator {
    private Simulator() {} // prevents instantiation

    public static abstract class Event {
        public double time;
        public abstract void trigger();
    }

    private static PriorityQueue eventSet = new PriorityQueue (
        (Event e1, Event e2)-> Float.compare( e1.time, e2.time )
    );

    public static void schedule( Event e ) {
        eventSet.add( e );
    }

    public static void run() {
        while (!eventSet.isEmpty()) {
            Event e = eventSet.remove();
            e.trigger();
        }
    }
}

Here, as in our extablished framework, the user is expected to schedule some events an then call run to launch the simulation. The difference is, instead of passing a lambda expression that will be evaluated at the appropriate time, the user constructs an event with a trigger method that will be called at the appropriate time.

Scheduling An Event in the New Framework, Take 1

To schedule an event in this new framework, we must create a new subclass of Simulator.event that does what interests us and then schedule it. Where do we do this? Wherever there was a call to Simulator.schedule in the previous version of the code.

Looking at class Road using the previous framework, we find only one call to Simulator.schedule. Here is our current version, with comments deleted:

    public void enter( double time ) {
        Simulator.schedule( time + travelTime, (double t)->leave( t ) );
    }

The most direct (but somewhat verbose) way to use our new framework here involves explicit declaration of a subclass of Simulator.event:

    public void enter( double time ) {
        Simulator.Event e = new RoadExit();
        e.time = time + travelTime;
        e.road = this;
        Simulator.schedule( e );
    }

The above assumes that we declare class RoadExit somewhere. Early in the development of Java, there were no inner classes, so we'd have to do something like this at the global level:

class RoadExit extends Simulator.event {
    public Road road;
    void trigger() {
        road.leave( time );
    }
}

If we use an inner class, we can make things a little nicer, although the machine code that results will be little different: Simulator.event:

    public void enter( double t ) {
        class RoadExit extends Simulator.event {
            RoadExit() {
                time = t + travelTime;
            }
            void trigger() {
                leave( time );
	    }
        }
        Simulator.schedule( new RoadExit() );
    }

In the above, we moved the initialization of time into the constructor for the RoadExit event, allowing us to directly schedule the new event without keeping a variable that references it.

A better framework

The above framework leaves many fields of events exposed to the public that need not be exposed. This creates the possibility that a careless programmer might do something like:

New event e = new RoadExit();
Simulator.schedule( e );
...
e.time = e.time + 3;

The problem with this is that once an event is in the pending event set, programmers should never modify the time field without informing the pending event set about the change. The easiest way to deal with this is to make the time field static:

import java.util.PriorityQueue;

public abstract class Simulator {
    private Simulator() {} // prevents instantiation

    public static abstract class Event {
        public final double time;
        Event( double time ) {
            this.time = time;
        }
        public abstract void trigger();
    }

    // the remainder is unchanged

By using a constructor, we could make the time final. This changes the code we need to schedule a new event: Now, when we create a subclass of events, we can use constructors similarly:

class RoadExit extends Simulator.event {
    public Road road;
    RoadExit( double time, Road road ) {
       super( time );
       this.road = road;
    );
    void trigger() {
        road.leave( time );
    }
}

This allows our scheduling code to be fairly nice looking:

    public void enter( double time ) {
        Simulator.schedule( new RoadExit( time + traveltime, this ) );
    }

Using inner classes cleans this up a bit more

    public void enter( double t ) {

	class RoadExit extends Simulator.event {
	    RoadExit() {
		super( t + travelTime );
	    }
	    void trigger() {
		leave( time );
	    }
	}

        Simulator.schedule( new RoadExit() );
    }

Why did RoadExit, as an inner class, not need to have an instance variable road? From the programmer's perspective, this is because Java's scope rules made the identity of the this road implicit and understood from the scope rules of Java. From the point of view of the Java implementation, this is because the compiler converts all inner classes to outer or global classes, with final variables added to hold each non-local variable referenced, and with parameteres added to the existing constructor to to pass those variables and lines added to the constructor to initialize them.

It is not too evil to smash the above code a little bit, but only if the constructor and the trigger method have very short one-line bodies:

    public void enter( double t ) {
	class RoadExit extends Simulator.event {
	    RoadExit() { super( t + travelTime ); }
	    void trigger() { leave( time ); }
	}
        Simulator.schedule( new RoadExit() );
    }

We wish we could do better. It would be nice to be able to use an anonymous inner class like this:

    public void enter( double t ) {
        Simulator.schedule(
	    new Simulator.Event( t + travelTime ) {
		void trigger() {
		    Road.this.exitEvent( time );
		}
	    }
	);
    }

Unfortunately, this is wishful thinking. Anonymous inner classes only seem to work with Java interfaces and not with Java abstract classes.

Evaluation

Looking back at our original code, the above code replaces the following:

    public void enter( double time ) {
        Simulator.schedule( time + travelTime, (double t)->leave( t ) );
    }

In sum, we've replaced 3 lines of code written with λ notation under the old framework with from 5 to 9 lines of code depending on how we opt to format the code.

It is important to note that once we've developed this rewrite of one call to schedule, we can use the same rewrite to fix all of the calls to schedule in the entire program, without the need to understand their context. As a result, we can rewrite code written for one framework with far less effort than was required to write that code in the first case.

You will probably never have to move a simulation program from one framework to another, but the same kind of rewriting is common when doing such things as moving an application from one graphical user interface to another, or moving an application from one operating system to another. Of course, this requires that the new framework be at least as expressive as the old. Moving code from a strong framework to a weak one frequently requires writing additional code to provide functions that are missing in the new framework.

What have we gained or lost by moving to this new framework? We are still using inner subclasses, these were used implicitly by λ expressions, but we are now using them explicitly. We are no longer using anonymous classes or λ expressions; this may help programmers who find that concept to be surprising or difficult.

If the programmers working on the project have a level of Java background typical of those who've finished a course like CS2, data structures, many of them will be utterly baffled by λ expressions. Those programmers will have an easier time dealing with added complexity in the code using elementary Java constructs than short sweet formulations using λ expressions.

Our code is a bit more verbose; lines of code is not a great measure of complexity, but note how the λ-expression version used just one method of class Simulator, while the new code uses both Simulator.schedule and the constructor Simulator.Event. This is real semantic complexity.

Lessons

Among the useful lessons to take away from this discussion is the fact that a change of simulation framework need not demolish the code using that framework. We've made a majore change to the user interface of our framework, but this did not force a rewrite of the entire simulation program. All it forced us to do is focus on each and every call to Simulator.scheule() rewriting the old versions into new versions that use the new framework.

This means that, while it is important to use a reasonably well designed framework for something like a simulation program, if you ever need to change the framework, everything is not lost. The vast bulk of the code to do the simulation is still valuable and will not change when you switch to a new framework. Essentially none of the application dependent code chages. The code that depends on the fact that this is a road-network simulation, and not, say, a digital-logic simulation or a neural-network simulation does not change.

Similar changes of framework occur when you take a Windows application and move it to MacOS or Linux, or even when you take a program written in, say, Python and rewrite it in, say, C++ (although the latter change may make the program much larger). The key is, all the investment in understanding the problem and debugging the original code will pay off under the new framework. The old code serves as a useful guide to writing the new code. Where English specifications of a program's behavior may be very ambiguous, leading to numerous debugging problems, you have unambiguous working code wot guide the change of framework.

This leads to a rule of thumb: "Translating a working program to a new language or system takes ten percent of the original development time or manpower or cost." (This saying was published by John Bently in 1985, and he attributed to me.) Our change of simulation framework is actually much less expensive than this saying estimates.