Respect Your Callers' Intentions

When we write code, we write it to be used. We call it from other places in our application. Others call it from places in their application. When we are writing code, we should show what we mean through our code, an idea known as intention-revealing interfaces. We reveal intentions through the external surface we expose. But what intentions are we exposing?

We write our code in a way that we think will be useful, but we are mostly writing implementation. We are writing what the code should do, and then from there how others would use it. The implementation drives the interface. The implementation does not always have the right intentions, though. Here's a simple class that stores historic temperature data for processing:


public class TemperatureHistory {
    private int[] temperatures;

    public TemperatureHistory(int[] temperatures) {
        this.temperatures = temperatures;
    }

    public int[] getTemperatures() {
        return temperatures;
    }
}

This is a simple data class. All it does is hold the temperature data, and there is no behavior on this class. In isolation, we don't entirely know if this class serves a valuable purpose or not, but we can look at the callers of this class to see how they are using it:


public int calculateAverageTemperature(TemperatureHistory temperatures) {
    int total = 0;
    for (int temperature : temperatures.getTemperatures()) {
        total += temperature;
    }

    return total / temperatures.getTemperatures().length;
}

public int findHighInLast30Days(TemperatureHistory temperatures) {
    int startIndex = Math.max(0, temperatures.getTemperatures().length - 30);
    int high = Integer.MIN_VALUE;

    for (int i = startIndex; i < temperatures.getTemperatures().length; i++) {
        if (temperatures.getTemperature()[i] > high) {
            high = temperatures.getTemperature()[i];
        }
    }

    return high;
}

public boolean shouldCoverPlants(TemperatureHistory temperatures) {
    int startIndex = Math.max(0, temperatures.getTemperatures().length - 7);

    for (int i = startIndex; i < temperatures.getTemperatures().length; i++) {
        if (temperatures.getTemperature()[i] <= 32) {
            // If any day in the last week was below freezing, we should cover plants
            return true;
        }
    }

    return false;
}

// etc...

We see that the interface of the TemperatureHistory class is not sufficient. Callers have to do a lot of work to massage the data returned into a useful form. The interface of the TemperatureHistory class was driven by the internal implementation of an array of integers. The calling code could make use of a better structure for their purposes. Some of this behavior would also be better suited to live in the TemperatureHistory class, hiding the implementation details completely from the callers. An improved TemperatureHistory class might look like:


public class TemperatureHistory {
    private int[] temperatures;

    public TemperatureHistory(int[] temperatures) {
        this.temperatures = temperatures;
    }

    public TemperatureHistory lastNDays(int days) {
        int startIndex = Math.max(0, temperatures.length - days);
        int actualDays = temperatures.length - startIndex;
        int[] window = new int[actualDays];

        for (int i = startIndex; i < temperatures.length; i++) {
            window[i - startIndex] = temperatures[i];
        }

        return new TemperatureHistory(window);
    }

    public int high() {
        int high = Integer.MIN_VALUE;

        for (int temperature : temperatures) {
            if (temperature > high) {
                high = temperature;
            }
        }

        return high;
    }

    public int low() {
        int low = Integer.MAX_VALUE;

        for (int temperature : temperatures) {
            if (temperature < low) {
                low = temperature;
            }
        }

        return low;
    }

    public int average() {
        int total = 0;
        for (int temperature : temperatures) {
            total += temperature;
        }

        return total / temperatures.length;
    }

    public int[] getTemperatures() {
        return temperatures;
    }
}

We have evolved the TemperatureHistory class to better suit the callers' intentions. It isn't perfect. We still have the getTemperatures method that doesn't tell us much, but we don't want to change all the code everywhere right this instant. There are other bits of the implementation that can be refactored, but we have focused our efforts on the interface first. Our callers can now be improved as well:


public int calculateAverageTemperature(TemperatureHistory temperatures) {
    return temperatures.average();
}

public int findHighInLast30Days(TemperatureHistory temperatures) {
    return temperatures.lastNDays(30).high();
}

public boolean shouldCoverPlants(TemperatureHistory temperatures) {
    return temperatures.lastNDays(7).low() <= 32;
}

// etc...

All of these callers were dramatically improved. Five or six lines each is reduced to a single line. Not every call will become a single line, but we have moved much of the common complexity into the TemperatureHistory class, leaving the callers to clearly declare what they actually want in language that is straightforward and concise.

It is the callers that determine what the interface of the code should be, not the implementation. We should always write code with the callers in mind. When we write from the callers' perspective, our code will be more useful and more valuable to them, benefiting everyone.

Comments

Popular posts from this blog

The Timeline of Errors

Magic Numbers, Semantics, and Compiler Errors

Assuming Debugging