35. Improving the Simulation Framework

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

 

Introduction

In the last lecture, we introduced this simulation 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();
                }
        }
}

To schedule an event in this new framework, we used code like this:

        void entryEvent( float t ) {
                // A vehicle arrives at the road entrance at time t

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

The above code is awkward because we had to create a named variable that we used just twice. It would be nice not to have to name it. Furthermore, we reached into a public field of that variable, time.

An Improved Framework

We can do better, but this involves some non-obvious code.

// Simulator.java

import java.util.PriorityQueue;

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

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

In the above, we have made the time field protected, preventing use of this field from anywhere except this class or its subclasses. In addition, we declared it to be final, so that it is, effectively, a read-only variable when accessed from anwhere but the initializer.

Because the final declaration does not give a value, the only legal way to initialize a variable of class Event is using an initializer that assigns a value to time. This prevents use of the default initializer and leaves only one possibility, the explicit initializer given.

Of course, it is illegal to initialize a variable of class Event because this is an abstract class. This raises a good question: Why is it even possible to declare an initializer here? The answer is simple: Subclasses of Event inherit this initializer.

An Improved Framework

Here is an illustration of the use of this new framework, in the context of the entryEvent method of class Road in our road-network simulator:

        void entryEvent( float t ) {
                // A vehicle arrives at the road entrance at time t

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

Here, we are creating an anonymous subclass of Simulation.Event giving a concrete implementation of the trigger() method that calls the exitEvent() method of the current Road.

Contrast the above code with this equivalent code from our λ-expression based framework:

        void entryEvent( float t ) {
                // A vehicle arrives at the road entrance at time t
                Simulator.schedule(
                        t+travelTime,
                        (float time)->Road.this.exitEvent( time )
                );
        }

What have we gained or lost by moving to this new framework? We are still using anonumous subclasses, an implicit feature of λ expressions, but we are now anonymous subclasses explicitly. We are no longer using λ expressions; this may help programmers who find that concept to be surprising or difficult. Our code is a bit more verbose; the λ-expression version did in 4 lines what our new code does in 7. 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 initializer 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.