Share

engineering

5min read

Maintaining Open-Source RxAndroidBle Library (API development)

Maintaining Open-Source RxAndroidBle Library (API development)

In the fourth part of the series devoted to maintaining RxAndroidBle library, we will discuss API development. If you want to come back to the introductory article, you can find it here. We’ve got API ready but we need to develop it.

Semantic Versioning (X.Y.Z)

This pattern is very handy when it comes to describing both to the user and the developer what may or may not change in between different versions of code. Let’s start from the end:

  • Z stands for the patch version. If this number changes in between versions of the code, it only means that the implementation detail changed but no public API was touched. It should be possible to simply switch the dependencies even in the already compiled code and run it successfully without any other actions.

  • Y is the minor version. Increasing this number allows the developer to add new API in the public interfaces of a module / library. All previously available APIs need to be in place although ABI may be changed (to see what ABI is just look below). A recompilation is needed.

  • X—the major version. Changing the major version means basically that users upgrading to this revision may need to sit down and rewrite some parts of their code to make it compile again. Previously available functions could not be available anymore.

For those who do not know—ABI stands for Application Binary Interface and can be considered an API for the computer. When users type for instance "Some String".substring(1) they use the API—when the computer compiles this line, it will translate the API to the ABI—the byte address of the method / function to be called. Binary interfaces also include types of data that are available—an int is not binary compatible with a char even though both of them contain numeric values. In Java world, changing the invocation of a method from void doSomething(int a) to void doSomething(int... manyAs) while being API compliant (invocation of both could look like doSomething(1)) is not the same when translated to the ABI. Does it mean that the developers are stuck with the need to support old, legacy API then? Yes, at least for some time. That is why the developers should understand the importance of designing the API before the official release and ask themselves a few questions:

  • What should be exposed as a public API (which will be needed to support)?

  • How to expose a smaller surface of the API (will it still get the work done)?

  • Will it be possible to translate the legacy API to the new one internally? (to support only the new one internally)?

Sometimes when developing a library you can make use of the findings from other fields, such as product development.

MVP or Minimal Viable Product could be a solution. The Pareto rule works in many different fields so why not to apply it here? 20% of effort can address the needs of 80% of potential users and most of them just want to get the job done as simply as possible. This approach can keep the API / functionality of the developed code lean and clear for the user and also easy for the developer to work on it further.

Ok, but we still have the 20% of the users that will need more customisation possible. How to address that?

Adding knobs

When it comes to extending the functionality of the code you develop, there are numerous approaches. All of them have pros and cons and should be weighted accordingly to an individual use-case. We will consider some of them.

Expose everything!

Let’s assume that we have only one functionality that we want to present to the user. Let’s call it void doSomething(). If the users would like to alter the behaviour of the method by adding a switch to perform the operation quickly, the API could look like this:

interface YourApi {
    void doSomething();
    void doSomething(boolean quickly);
}

Most of the users would still use .doSomething() as it suits their needs and they don’t have to know what exactly the quick switch does. Still, there can be another option to alter how the method mightbe called. It can also be done by adding a parameter int repeatCount. Most users will still use the version without parameters and some of them will just use one or the other parameter. So now we would have:

interface YourApi {
    void doSomething();
    void doSomething(boolean quickly);
    void doSomething(int repeatCount);
    void doSomething(boolean quickly, int repeatCount);
}

But you can imagine that if the next parameter is needed, the method overload count will increase rapidly. This can be still a valid approach as some well known libraries use APIs like this (i.e. RxJava Observable API). Although it is efficient to use once someone is familiar with it, it is pretty difficult to learn.

Telescopic constructor pattern

Sometimes you as the developer know that some API can get more defined only in one—linear—way. This is sometimes used in Java object constructor pattern, which looks like this:

class YourClass {
    YourClass() {
        this(false);
    }
    YourClass(boolean preciseParameter) {
        this(preciseParameter, 0);
    }
    YourClass(boolean preciseParameter, int morePreciseParameter) {
        this(preciseParameter, morePreciseParameter, null);
    }
    YourClass(boolean preciseParameter, int morePreciseParameter, @Nullable Object evenMorePreciseParameter) {
        // the only one to support
    }
}

This may leave you with just one API to support as the rest of the calls are using it. Unfortunately, if another parameter appears and needs to be added, this situation will get very similar to the ‘expose everything’ approach.

Strategy builder pattern

What seems to be the best fit for our Bluetooth libraries so far is the Strategy builder pattern.

interface StrategyBuilderApi {
    void doSomething(int repeatCount); // the most simple version of the API
    void doSomething(int repeatCount, Strategy strategy);
}

The Strategy will contain all of the possible switches that can be used for a particular call and the Strategy.Builder will allow for easy extension.

class Strategy {
    final boolean firstParameter;
    final int secondParameter;
    final Object thirdParameter;

    private Strategy(boolean firstParameter, int secondParameter, Object thirdParameter) {
        this.firstParameter = firstParameter;
        this.secondParameter = secondParameter;
        this.thirdParameter = thirdParameter;
    }

    static class Builder {
        boolean firstParameter = false;
        int secondParameter = 0;
        Object thirdParameter = null;

        /**
        * The description for the first parameter
        */
        void setFirstParameter(boolean firstParameter) {
            this.firstParameter = firstParameter;
        }

        /**
        * The description for the second parameter
        */
        void setSecondParameter(int secondParameter) {
            this.secondParameter = secondParameter;
        }

        /**
        * The description for the third parameter
        */
        void setThirdParameter(Object thirdParameter) {
            this.thirdParameter = thirdParameter;
        }
    }
}

Using this approach for adding more knobs to the API does not create a need for expanding the surface of the API. It also eases the strain for the new users who can learn how to alter the behaviour of the API by reading the Javadoc descriptions of individual parameters one by one. There are more possibilities of course, so definitely devote some time to choose the best strategy.

Share

Darek

Staff Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!