25 Chained Mutators

Examples abound in which a method is invoked on the value returned by another method without assigning the returned value to an intermediate variable. Opinions differ, among programmers at all levels, about the efficacy of this practice. Nonetheless, it is quite common. Hence, it is important to consider how this behavior can be taken into account when implementing methods that might be chained in this way.

Motivation

Suppose you need to construct an email address from a String variable named user containing the user’s name and a String variable named university containing the university’s name. Suppose, further, that for efficiency reasons you want to use a StringBuilder rather than String concatenation.

One way to implement this is as follows:

        StringBuilder sb = new StringBuilder();
        sb.append(user);
        sb.append("@");
        sb.append(university);
        sb.append(".edu");

In this implementation, each call to append() simply modifies the state of the StringBuilder as required.

While this works, it’s somewhat messier than using String concatenation, because it requires multiple statements. What you might like to do, instead, is take advantage of the efficiency of the StringBuilder class without the added messiness. In principal, you could accomplish this using invocation chaining as follows:

        StringBuilder sb = new StringBuilder();
        sb.append(user).append("@").append(university).append(".edu");

However, this will only work if the append() method is implemented with this kind of functionality in mind.

Review

Now, consider a different, but related, example. Suppose you are working with a File object that encapsulates the current working directory, and you want to know how many characters are in its name. This can be accomplished as follows:

        File   cwd    = new File(".");
        String path   = cwd.getCanonicalPath();
        int    length = path.length();

However, since there’s no need for the intermediate variable path, many people prefer the following chained implementation:

        File   cwd    = new File(".");
        int    length = cwd.getCanonicalPath().length();

This chained implementation works because the getCanonicalPath() method in the File class returns (and evaluates to) a String object, and the String class has a length() method.

Thinking About The Problem

Though they are similar on the surface, the email example and the path example are quite different. In the path example, the methods do not change the state of their owning objects. That is, the getCanonicalPath() method is an accessor not a mutator (i.e., it does not change the state of its File object, it returns a String object) and the length() method is also an accessor not a mutator (i.e., it does not change the state of its String object, it returns an int). On the other hand, in the email example, the append() method does change the state of its StringBuilder (i.e., it is a mutator and does not need to return anything).

So, while it is easy to see why you can use invocation chaining in the path example, it does not seem like you should be able to do so in the email example. Indeed, in order for you to be able to use invocation chaining in the path example, the append() method must return the StringBuilder that it is modifying.

The Pattern

This motivating example can be generalized to create a pattern that solves the chained mutator problem. Specifically, if you want to be able to use invocation chaining to change an object, then the mutator methods that are to be chained must return something that a subsequent method in the chain can be invoked on. But, it can’t just return anything; it must return an object of the appropriate type (i.e., an object of the same type as the owning object). But, even that isn’t enough — it must actually return the owning object itself. That is, the mutator must return the reference to the owning object, this.

The StringBuilder class uses this idea, for exactly this reason. The methods append(), delete(), insert(), replace(), and reverse() all return this so that their invocations can be chained. So, in the example, sb.append(user) returns this (i.e., a reference to sb), append("@") is then invoked on sb, and so on.

An Example

Suppose you want to create an encapsulation of a Robot that keeps its location in one or more attributes and is able to move in four directions (forward, backward, right and left). You clearly need one or more mutator methods to handle the movements. Suppose further that you decide to have a mutator method for each direction, named moveBackward(), moveForward(), moveLeft(), and moveRight().

If you were not interested in supporting invocation chaining, then these methods would be void (i.e., they would not return anything), and they could be used as in the following example:

        Robot bender = new Robot();
        bender.moveForward();
        bender.moveForward();
        bender.moveRight();
        bender.moveForward();

However, if you are interested in invocation chaining, these methods must, instead, return a reference to the owning object, as in the following implementation:

public class Robot {
    private int x, y;

    public Robot() {
        x = 0; y = 0;
    }

    public Robot moveBackward() {
        y--;
        return this;
    }

    public Robot moveForward() {
        y++;
        return this;
    }

    public Robot moveLeft() {
        x--;
        return this;
    }

    public Robot moveRight() {
        x++;
        return this;
    }

    public String toString() {
        return String.format("I am at (%d, %d).", x, y);
    }
}

You can then use this object as follows:

        Robot bender = new Robot();
        bender.moveForward().moveForward().moveRight().moveForward();

Many people think the chained invocation is much more convenient than having to use a separate statement for each movement. If, however, you want to have a separate statement for each movement you can; you just ignore the return value (as in the original example). In other words, providing the ability to chain invocations has no disadvantages, only advantages.

A Warning

It is very important to document chainable mutator methods, and methods that look like chainable mutator methods, carefully. This need is made apparent by the String and StringBuilder classes.

For example, the toLowerCase() and toUpperCase() methods in the String class could easily be mistaken for mutators. In fact, if you didn’t know that String objects were immutable, you would almost certainly think this was the case. The clue that they are not mutators is the fact that they return String objects. In other words, the fact that they return a String object is a clue that they construct a new String object from the owning String object and return the new object.

As another example, the Color class has brighter() and darker() methods that you might think, from their names, are mutators. Again, however, Color objects are immutable, and one clue is that these methods return Color objects.

However, it is important not to over-generalize. As you’ve now seen, many mutator methods in the StringBuilder class return StringBuilder objects. In this case, it is to support invocation chaining. The only way to know is to read the documentation.

So, it is very important to carefully document methods in immutable classes that might appear to be mutators and mutators in mutable classes that support invocation chaining. It is easy to make inappropriate assumptions about the way an object will behave, based only on method signatures. The only way to prevent the problems that arise from such assumptions is to document the code.

License

Icon for the Creative Commons Attribution 4.0 International License

Patterns for Beginning Programmers Copyright © 2022 by David Bernstein is licensed under a Creative Commons Attribution 4.0 International License, except where otherwise noted.

Share This Book