'. '

AlternativeBehavior

From APIDesign

Revision as of 08:19, 8 November 2010 by JaroslavTulach (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

As has been explained in ClarityOfAccessModifiers page, sometimes even relatively fine looking API can have a hidden catch. Just like following Arithmetica class:

Code from Arithmetica.java:
See the whole file.

public class Arithmetica {
    public int sumTwo(int one, int second) {
        return one + second;
    }
 
    public int sumAll(int... numbers) {
        if (numbers.length == 0) {
            return 0;
        }
        int sum = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            sum = sumTwo(sum, numbers[i]);
        }
        return sum;
    }
 
    public int sumRange(int from, int to) {
        int len = to - from;
        if (len < 0) {
            len = -len;
            from = to;
        }
        int[] array = new int[len + 1];
        for (int i = 0; i <= len; i++) {
            array[i] = from + i;
        }
        return sumAll(array);
    }
}
 

Without getting into details (more in ClarityOfAccessModifiers), let's assume the problem is clear and that we all understand that releasing version 2.0 of that class with following reimplementation of the sumRange method would clearly be an incompatible API change:

Code from Arithmetica.java:
See the whole file.

public int sumRange(int from, int to) {
    return (from + to) * (Math.abs(to - from) + 1) / 2;
}
 

As the whole API Design methodology advocates as little of incompatible changes as possible, it shall be clear that we have a problem. What can we do? Is all our API development in stuck? Do we need to keep the old, slow 1.0 version of sumRange method? Do we have to sacrifice BackwardCompatibility? No and no. We just need to learn how to provide AlternativeBehaviours.

Contents

Compile-time Alternatives

To achieve easy upgradability, it helps to do as little shaking of the amoeba shape of our library as possible. AlternativeBehaviour that requires an API user to do conscious action during compilation is one of the most straightforward ways to reach such nirvana. Imagine that in version 2.0 the Arithmetica class is enhanced not only by new version of the sumRange method, but also by a new constructor:

Code from Arithmetica.java:
See the whole file.

public class Arithmetica {
    private final Version version;
    public enum Version { VERSION_1_0, VERSION_2_0 }
 
    public Arithmetica() {
        this(Version.VERSION_1_0);
    }
    public Arithmetica(Version version) {
        this.version = version;
    }
 
    public int sumRange(int from, int to) {
        switch (version) {
            case VERSION_1_0:
                return sumRange1(from, to);
            case VERSION_2_0:
                return sumRange2(from, to);
            default:
                throw new IllegalStateException();
        }
    }
}
 

Clearly, the whole change now becomes backward compatible. Those users who don't change their code continue to use the default constructor and as such they see the exact behaviour as in version 1.0. Those who decide to use the new constructor and request the version 2.0, can benefit from faster algorithm (and slight incompatibility).

However to trigger the new behaviour requires a conscious decision to update own code. And developers are used that if they touch their code things can get broken. Everything is good.

One may say that the enum Version is not really nice. True, but often one can enhance an API with new factory methods, or overloaded constructor with more meaningful arguments than enum.

Deploy-time Alternatives

Requiring changes to code and recompilation in order to use new AlternativeBehaviour works well for changes that shall be consumed by developers. However, not only developers are responsible for production of our software systems. Our systems are often assembled from pieces by people who do not want to deal with the code at all.

Such administrators focus on deploying the system pieces/modules and configuring them to work together. For them it is better to provide deploy time way to influence and choose from various AlternativeBehaviours. For example using a system property:

Code from Arithmetica.java:
See the whole file.

public class Arithmetica {
    public int sumTwo(int one, int second) {
        return one + second;
    }
 
    public int sumAll(int... numbers) {
        if (numbers.length == 0) {
            return 0;
        }
        int sum = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            sum = sumTwo(sum, numbers[i]);
        }
        return sum;
    }
 
    public int sumRange(int from, int to) {
        if (Boolean.getBoolean("arithmetica.v2")) {
            return sumRange2(from, to);
        } else {
            return sumRange1(from, to);
        }
    }
 
    private int sumRange1(int from, int to) {
        int len = to - from;
        if (len < 0) {
            len = -len;
            from = to;
        }
        int[] array = new int[len + 1];
        for (int i = 0; i <= len; i++) {
            array[i] = from + i;
        }
        return sumAll(array);
    }
 
    private int sumRange2(int from, int to) {
        return (from + to) * (Math.abs(to - from) + 1) / 2;
    }
}
 

Tim Band in his review objected against global wide switches like in the previous case and I have to agree with him: Yes, this sort of transfers the responsibility for the incompatible change on the shoulders of the assemblers of the final system, without really solving the incompatibility itself. True. However sometimes this is what you want. Also the property does not need to be global, there can be finer grained configuration domains and then this style of alternatives becomes viable solution for those who assemble the final systems.


Side by Side Alternatives

Sometimes the best way to provide an alternative is to simply duplicate the class. One can have Math and StrictMath or StringBuffer and StringBuilder. Then the decision to use the old or new version needs to be done in the compile time as well.

The biggest problem in this case is to exchange data structures between the old and new version. Sometimes this is easy (like exchanging numbers between Math and StrictMath), however often it is not easy at all and one needs to either give up on the task on invest in writing a bridge.

Ghost Alternatives

With a little bit of help from some Modular Runtime Container one can even load two versions of some class with two different ClassLoaders. The decision to load the old or new class is done during linkage time, based on dependencies.

If the runtime container sees a request to link against module 1.6 offering HashMap, it can load the old version. As soon as the container sees a request to load module 1.7 or newer, it can load the new, enhanced, yet slightly incompatible HashMap implementation.

This looks a bit more comfortable than the side by side coexistence of Math and StrictMath, as the only change that is necessary to upgrade from older version to new improved version is to upgrade the module dependencies.

Yet the biggest problem remains the same. In case there are two modules in the VM and one requires the old and one the new version, then the two HashMaps classes are incompatible types and their instances are not interchangable and cannot traverse from one module to another.

Runtime-time Alternatives

Solution that offers as simple upgrade path as the ghost alternatives and moreover it solves the problem of mutual incompatible types is based on runtime inspection. The basic presumption is that the code carries runtime information about its necessary dependencies which can be queried during runtime. Then one can rewrite the Arithmetica class in following way:

Code from Arithmetica.java:
See the whole file.

public class Arithmetica {
    public int sumTwo(int one, int second) {
        return one + second;
    }
 
    public int sumAll(int... numbers) {
        if (numbers.length == 0) {
            return 0;
        }
        int sum = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            sum = sumTwo(sum, numbers[i]);
        }
        return sum;
    }
 
    public int sumRange(int from, int to) {
        if (calledByV2AwareLoader()) {
            return sumRange2(from, to);
        } else {
            return sumRange1(from, to);
        }
    }
 
    private int sumRange1(int from, int to) {
        int len = to - from;
        if (len < 0) {
            len = -len;
            from = to;
        }
        int[] array = new int[len + 1];
        for (int i = 0; i <= len; i++) {
            array[i] = from + i;
        }
        return sumAll(array);
    }
 
    private int sumRange2(int from, int to) {
        return (from + to) * (Math.abs(to - from) + 1) / 2;
    }
 
    private static boolean calledByV2AwareLoader() {
        StackTraceElement[] arr = Thread.currentThread().getStackTrace();
        ClassLoader myLoader = Arithmetica.class.getClassLoader();
        for (int i = 0; i < arr.length; i++) {
            ClassLoader caller = arr[i].getClass().getClassLoader();
            if (myLoader == caller) {
                continue;
            }
            if (RuntimeCheck.requiresAtLeast(
                "2.0", "org.apidesign.math", caller
            )) {
                return true;
            }
            return false;
        }
        return true;
    }
 
}
 

By doing this inspection the Arithmetica class can find out what actual version and thus behaviour the code calling into the API really requires. If the dependency is on anything older than version 2.0 the code will see the old behaviour. Callers compiled against the newer, improved sumRange implementation will benefit from the performance boost. Moreover there is just one version of the Arithmetica class in the whole system, so there are no problems with multiple ghost types.

The above example assumes that there is 1:1 mapping between a module
and a classloader and as such it is easy to iterate the stack and find
the dependency information from it. This is the simplest case, good for
demonstration purposes. Everything else would just complicate the principle.

This style of providing AlternativeBehaviours is not really common in Java these days, but it is worth to mention that this is exactly the system used by C linker. More about that in How to Write Shared Libraries.

Personal tools