Books / Patterns for Beginning Programmers / Chapter 31
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.
In this chapter
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.