A serial killer in the space-time continuum

serial_parallel.jpg

I had to simulate how weather affected the the race cars in the motor racing simulation I wrote about earlier. Weather is randomly chosen: there is a probability of rain for each race, each rain shower and dry period has a random duration, the rate of rainfall is randomised, there is a random temperature that affects tyre wear and the rate at which the track dries off, and so on.

I started with a Weather object that had a probability of rain and a random temperature, within some bounds. The Weather object randomised itself by reading random numbers between 0 and 1 from a Random object. This was easy to test: I used a mock random number generator that expected a sequence of two calls to nextDouble, the first of which was used to decide if it was raining and the second to calculate temperature.

I then extended the class to randomise the heaviness of rainfall. This broke my existing tests. Obviously, where the Weather object had asked for two random numbers it was now asking for three random numbers. Easy to fix, but my tests were still failing! Why?

It turned out that while adding random heaviness I had reordered the randomisation of the temperature and probability of rain to make the code easier to read. Lesson 1: I should not have been refactoring while adding functionality. But the goof made me realise something much more interesting. The true problem was not that I had mixed refactoring and implementation, but that the Weather object was making a sequence of calls to the same method of another object without there actually being a true temporal relationship between those calls. Each random number returned by the generator was, as far as the Weather object was concerned, completely independent of any other - that's the very definition of random, after all.

So, I changed the Weather object to hold multiple references to random number generators, one for each aspect of its state that it wanted to randomise. In tests it was passed multiple mock random number generators, so I could control each aspect of its state. In the production system it was passed multiple references to the same random number generator through a convenient constructor.

class Weather {
    public Weather( Random temperatureRandom, 
                    Random isRainingRandom, 
                    Random rainfallRandom )
    {
        ...
    }

    public Weather( Random randomness ) {
        this( randomness, randomness, randomness );
    }
}

But that gave me the uncomfortable feeling between my shoulder-blades that I always get when looking at bad code. All those Random parameters were an obvious code smell: too many parameters indicate that a new object should be factored out. And I could see that every time I added functionality to the Weather class I would need to add more random number generators, making the first constructor signature really awkward and, worse, I would break all existing code that uses that constructor.

So, I pulled the random number generators into a Weather-specific source of randomness with an abstract interface:

interface WeatherRandom {
    double nextTemperature();
    double nextChanceOfRain();
    double nextRainfall();
}

Now I could easily mock that interface in my tests and use a simple adapter in the running system that delegated to a Random object. This had the advantage that I when added functionality to the Weather class I only needed to add methods to the WeatherRandom interface, but didn't need to change any test code that tests existing random behaviour.

It is often the case that, as in this example, one object will make multiple calls to a single method of another. Such a design can lead to brittle tests if there is no true temporal relationship between those calls. That is, if the order of outgoing calls is not important to the functionality of the object under test, the tests should not place constraints on that order. If they do, changes to the object's internals might break unrelated tests. The solution is to refactor a sequence of calls to the same method into calls to different methods that can occur in any order.

One way to view these refactorings is that I replaced a serial interface with a parallel interface between the Weather and object and random number generator. Another viewpoint is that I used a spatial construct to describe a temporal aspect of my program's behaviour. The relationship between time and space in programming is something that James Coplien has written much about [1,2,3].

Copyright © 2003 Nat Pryce. Posted 2003-11-25. Share it.

Comments powered by Disqus