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.