Innocent integration, haunted by ghosts

Java Systemutveckling Teknologi Bugs Debugging

Uzi Landsmann

Systemutvecklare

Working as a Java backend developer, I once wrote a very simple service that was used as a central point for saving and retrieving information about radio antennas’ location and equipment. The location part included the site’s coordinates, and the idea was to allow saving and fetching them in different formats. For this particular requirement, my service had to integrate with a third-party coordinate transforming software. This innocent-looking integration had caused the most peculiar bugs, made me suspect that my implementation was haunted by ghosts, and finally changed the way I think about software altogether. Read on to find out how and why.


My Location Service was simple enough, as can be seen in the drawing below:

Innocent integration, haunted by ghosts - debugging


It supplied other systems, let’s call them Systems A, B, and C, with a central integration point where they could save and retrieve information about different sites’ locations. The Location service, in turn, saved and retrieved the information by talking to a database. The integration with the third-party Coordinate Converter service made the transformation between different coordinate systems possible.


So, in Sweden, where this story had taken place, there were three major coordinate systems at the time, called RT90, WGS84, and SWEREF99. The third-party service had static methods that could take coordinates in one system and return them as one of the others. When integrating with the converter service, I wrote some tests against it that worked flawlessly and I could convert a coordinate from one system to the second and then to the third and back again to the first one without losing too much precision. The converter service worked like a charm.


I wrote my location service, integrated it with the converter service, and added CRUD operations to save and fetch data from the database. I also added a lot of tests because it was way too easy to mess things up with different coordinate systems and the likelihood that your location ends up in the middle of the pacific ocean is overwhelming if you’re not careful enough.


The problems began showing up when I started writing integration tests that intended to test data flows between, say, System A, the location service, the converter service, and the database. For some reason, the data that was finally saved in the database differed from the data that was used originally in System A. This puzzled me and I assumed that I had obviously done something stupid when converting the coordinates and saving them in the database. Considering that mixing the X’s and Y’s is one of my specialties, I wasn’t too surprised to find out that something went wrong, so I sat up and decided to debug the program.


This took…days. I just couldn’t figure out what was wrong. I received the coordinates from system A, then transformed them to other coordinates systems, and finally saved all of them in the database. And they were wrong. That is, the original coordinates, the ones supplied by system A, were changed. Not much, mind you, but a few decimals were off. The system, I concluded, was haunted by ghosts. Nothing else could explain it.


Finally, after a lot of coffee and profanities, I took a good look at the coordinates before and after the transformation. I used a simple data class, called Coords, which was supplied by the converting service and used both as a parameter and as a return value. It looked something like this:

package third.party;

public class Coords {
    public double x, y;

    public Coords(double x, double y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "(" + x + "," + y + ")";
    }
}


And the service conversion method looked like this:

package third.party;

public class CoordsConverter {
    public static Coords fromRT90ToWGS84(Coords coords) {
        // some complex logic
    }
    
    // more conversions
}


In order to keep an eye on things while debugging, I added both the original and converted coordinates as watches in my debugger, to see exactly when I messed up and changed them. Stepping through my uncomplicated conversion logic, one row at a time, I was prepared to hit myself on my forehead as soon as I’d encountered the stupid mistake, but then suddenly realized something that shook me totally: it wasn’t me. It was them.


The third-party coordinate converter. It returned a new instance of the converted coordinates as promised. But while doing so it also changed the values of the coordinates that were sent to it as arguments.


I then realized that I should have listened closely when my mother gave me that very important lesson:

“Never trust anyone”

- Mom

 

Following that very wise advice, I considered how to deal with that piece of untrustworthy software. I came up with a solution that involved using the facade design pattern Länk till annan webbplats..

 

First, I created an immutable coordinates class. I wanted to keep the x and y public, but now I knew that no one would ever change them once they were created:

package some.system;

public class ImmutableCoords {
    public final double x;
    public final double y;

    public ImmutableCoords(final double x, final double y) {
        this.x = x;
        this.y = y;
    }
}

 

Then I created a facade class that created a temporary instance of the coordinate needed for the transformation. Once converted, it created a new immutable coordinate instance with the correct values and returned it. With this class in place, the rest of my implementation will never have to deal with the unreliable converter class directly, as all calls to it would be redirected through the safe facade:

package some.system;

import third.party.Coords;
import third.party.CoordsConverter;

public class UntrustworthyCoordsConverterFacade {
    public static ImmutableCoords fromRT90ToWGS84(ImmutableCoords rt90) {
        Coords coordsToSend = new Coords(rt90.x, rt90.y);
        Coords transformedCoords = CoordsConverter.fromRT90ToWGS84(coordsToSend);

        return new ImmutableCoords(
                transformedCoords.x,
                transformedCoords.y);
    }

    // more methods
}

 

So what is the moral of the story? A few things actually:

  • Don’t trust third-party software blindly, sometimes even other people make mistakes

  • Try to always use immutable classes. It will save you a ton of time when ugly bugs are out there to get you. Use final variables when possible, for the same reason

  • Name your classes and variables appropriately, it helps

  • Listen to your mom

Fler inspirerande inlägg du inte får missa