34. Changing the Simulation Framework

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

 

Introduction

We have created a road-network simulation and a logic 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 withing 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:

// Simulator.java

import java.util.PriorityQueue;

/** Framework for discrete event simulation
 *  @author Douglas Jones
 *  @version 4/18/2016
 */
class Simulator {

    /** Users create subclasses of event to schedule anything
     */
    public static abstract class Event {
        public float time;       // the time of this event
        abstract void trigger(); // what to do at that time
    }

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

    /** Call schedule to make act happen at time.
     */
    public static void schedule( Event e ) {
        eventSet.add( e );
    }

    /** Call run() after scheduling some initial events
     *  to run the simulation.
     */
    public static void run() {
        while (!eventSet.isEmpty()) {
            Event e = eventSet.remove();
            e.trigger();
        }
    }
}

Scheduling An Event in the New Framework, Take 1

To schedule an event in this new framework, we must create a new implementation of class 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, but with simulation output code and notes about improvement deleted:

    /** what happens when a vehicle enters this road
     *  @param time the vehicle enters
     */
    public void entryEvent( float time ) {
        // after a vehicle enters the road, it exits it travelTime later
        Simulator.schedule(
            time + travelTime,
            (float t)->exitEvent( 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 entryEvent( float t ) {
        Simulator.Event e = new RoadExit();
        e.time = t + travelTime;
        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 {
    void trigger() {
        Road.exitEvent( time );
    }
}

Using a Constructor in the new framework

We have the option of using a constructor to initialize the time field of the new event. In this case, the code to schedule the event looks like this:

    public void entryEvent( float t ) {
        Simulator.schedule( new RoadExit( t + travelTime ) );
    }

And class RoadExit looks like this:

class RoadExit extends Simulator.event {
    void trigger() {
        Road.exitEvent( time );
    }
    RoadExit( float t ) {
	time = t;
    }
}

If this was the only framwork we had, we'd probably move all of the code of Road.exitEvent into the trigger method, a move that might require us to make fields and methods of class Road public in order for us to access them from class RoadExit.

Note that if we always set the time field of the event in a constructor, we can make Event.time a protected field instead of a public field.

Using Inner Classes in the new framework

With the introduction of inner classes, Java offers an alternative. Make class RoadExit an inner class, local to the point where it is scheduled. In this case, we end up with the following:

    public void entryEvent( float t ) {

	class RoadExit extends Simulator.event {
	    void trigger() {
		Road.exitEvent( time );
	    }
	    RoadExit( float t ) {
		time = t;
	    }
	}
        Simulator.schedule( new RoadExit( t + travelTime ) );

    }

We can do better than this. Java allows anonymous constructors; these are particularly useful when there are no parameters. Because this is an inner class, we can reach out from the anonymous constructor to find t and travelTime instead of passing their sum to the constructor as a parameter:

    public void entryEvent( float t ) {

	class RoadExit extends Simulator.event {
	    void trigger() {
		Road.exitEvent( time );
	    }
	    {
		time = t + travelTime;
	    }
	}
        Simulator.schedule( new RoadExit() );

    }

This only works if t and travelTime are final or effectively final. In the case of travelTime, this is trivial, since that field of class Road is declared to be final. In the case of t we can demonstrate that it is effectively final by noting that there are no assignments made to this parameter anywhere in the code. Given that this is the case, we have the option of explicitly declaring t to be final.

Using the new Framework with anonymous inner classes

This code is verbose, and it is always annoying to be forced to invent a class name that is used only once. The name we invented, RoadExit is not very nice, in part, because the division of responsibility between it and the method it calls, ExitEvent is not really obvious except for the fact that we inherited ExitEvent from the previous context. Fortunately, this name is purely local, so awkward names are not too great a problem.

Had we started out with this scheduling framework, we'd have avoided using the suffix Event on anything that wasn't a subclass of Simulation.Event. We didn't start with this framework, though, so we're stuck unless we want to undertake a global renaming.

Java allows us to create anonymous subclasses. This allows us to eliminate the need for the name ExitEv used above and it allows for a more compact notation, although not as compact as we could accomplish with λ expressions: default initializer of the parent class without any inline initialization code, and Java can handle it.

    public void entryEvent( final float t ) {

        Simulator.schedule(
	    new Simulation.Event() {
		void trigger() {
		    Road.this.exitEvent( time );
		}
		{ time = t + travelTime; }
	    }
	);

    }

Evaluation

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

    public void entryEvent( float time ) {

        Simulator.schedule(
            time + travelTime,
            (float t)->exitEvent( t )
        );

    }

In sum, we've replaced 4 lines of code written with λ notation under the old framework with 8 lines of code written with an anonymous subclass class of Event under our new framework.

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.