Influence of fluent APIs on Refactoring

Refactoring is applying a transformation to source code, which does not change the code's behaviour.

Then, one might ask - what is the purpose of such transformations? It is to change the structure of the code, which results in better maintainability.

In this post, we explore an interesting phenomenon. That is how a particular structure of the code lends itself to refactoring in a better way.

Let us understand what fluent APIs are with an example. Consider the following Java code.

public class FluentApiDemo {
    public static void main(String[] args) {
        final StringBuilder accumulator = new StringBuilder();
        accumulator.append(10);
        accumulator.append("*");
        accumulator.append("(");
        accumulator.append(20);
        accumulator.append("+");
        accumulator.append(30);
        accumulator.append(")");
        System.out.println(accumulator.toString()); // 10*(20+30)
    }
}

We are trying to construct an expression in this example. For simplicity, I have hardcoded the values. In the real world, these values will come from the users.

The variable accumulator holds the state as the expression evolves. The class StringBuilder contains several overloaded append methods to support multiple types of arguments.

In the above example, it appears as if the append methods are void. It is because the instance of StringBuilder will change its state to hold the result of appending, and we don't need anything to be returned from the method to achieve the intended functionality.

However, if you look at the source code of StringBuilder, you will find that the methods return the corresponding StringBuilder objects.

public StringBuilder append(String str);

At the outset, it might appear like this is useless.

If you look at our code, you will see that the variable accumulator has been repeated several times, which makes it a noise. What I mean is that when we write this piece of code, we know that we want to perform multiple appends to the variable accumulator. With this mindset, when we repeatedly type the variable accumulator, our attention to it keeps reducing each time we type it. This might lead to using another similarly named variable, resulting in a bug.

Let's start transforming the above code by assigning the returned value to the variable accumulator after each append. To make this happen, we must let go of the final modifier for accumulator.

public class FluentApiDemo {
    public static void main(String[] args) {
        StringBuilder accumulator = new StringBuilder();
        accumulator = accumulator.append(10);
        accumulator = accumulator.append("*");
        accumulator = accumulator.append("(");
        accumulator = accumulator.append(20);
        accumulator = accumulator.append("+");
        accumulator = accumulator.append(30);
        accumulator = accumulator.append(")");
        System.out.println(accumulator.toString());
    }
}

When the methods of a class return the object on which the method was invoked, they create fluent APIs.

Now, let us start inlining the variable accumulator from the bottom to the top. We will end up with the following code.

public class FluentApiDemo {
    public static void main(String[] args) {
        StringBuilder accumulator;
        accumulator = new StringBuilder()
                .append(10)
                .append("*")
                .append("(")
                .append(20)
                .append("+")
                .append(30)
                .append(")");
        System.out.println(accumulator.toString());
    }
}

By looking at the chained method calls, we observe the following.

  1. The variable accumulator is not repeated, leading to lesser noise and concise code.
  2. There is a lesser chance of using the wrong variable because of the lack of repetitions.

Suppose our code is not producing the intended output, and we want to inspect the state after appending 20; we can easily achieve that by applying 'extract variable' refactoring.

public class FluentApiDemo {
    public static void main(String[] args) {
        StringBuilder accumulator;
        accumulator = new StringBuilder()
                .append(10)
                .append("*")
                .append("(")
                .append(20);
        System.out.println(accumulator.toString());
        accumulator = accumulator
                .append("+")
                .append(30)
                .append(")");
        System.out.println(accumulator.toString());
    }
}

Thus, the fluent API allows us to transform the code structure easily by applying either "introduce/extract variable" or "inline variable" transformations.

The beauty of the code lies in its evolvability. Refactoring skills help developers to keep the code evolvable.