Saturday, March 26, 2011

Immutability and builders

Once you get to a certain point in working with Java, you always keep in mind how your code will work in a multi-threaded environment. It isn't a strange thing to think about: servlets run with threads, as do EJBs and Swing applications. Even if you are not dealing with threads now, you could be in the future, or someone else might try to apply your code to threads.

One of the easiest ways to handle the question "Will this work with threads?" is to make your classes immutable. The state of an immutable object cannot be changed after its construction. If an object is immutable, then there is no chance that threads will see different state in the object when they shouldn't. Immutability also leads to a simpler API for your class and overall more predictable behavior.

Here's a typical immutable class.
public class USAddress {
  private final String streetAddress;
  private final String city;
  private final String state;

  public USAddress (String a, String c, String s) {
    streetAddress = a;
    city = c;
    state = s;
  }
  public String getStreetAddress() { return streetAddress; }
  public String getCity() { return city; }
  public String getState() { return state; }
The fields in this class are all final, which means two things: first, that they can only be assigned once; second, that they must be assigned after construction. These properties of final help make the class immutable.

I didn't include implementations of equals() or hashCode(), but I'll just say that they would depend on the fields in the class. Immutability implies that the hash code of a USAddress object never changes, which is great because it will never get lost in a hash table. For some objects, if there are a lot of computations involved for generating a hash code, you could just calculate it once and cache it internally for speed.

One downside to immutable classes is that they must have all their data passed to them on construction. Let's expand the class above and see what happens. I'm going to leave out the getter methods.
public class USAddress {
  private final String streetAddressLine1;
  private final String streetAddressLine2;
  private final String city;
  private final String state;
  private final String zipCode;
  private final boolean isPostOfficeBox;
  private final boolean isAptOrCondo;

  public USAddress (String a1, String a2, String c, String s, String z,
      boolean p, boolean ac) {
    streetAddressLine1 = a1;
    streetAddressLine2 = a2;
    city = c;
    state = s;
    zipCode = z;
    isPostOfficeBox = p;
    isAptOrCondo = ac;
  }
  // ... getters ...
}
The problem is the constructor. It has five strings in a row and then two booleans in a row. It could be tricky to remember the right order of the parameters, which one is which.
USAddress a = new USAddress ("1234 Elm Street", "Apt. 56",
    "Springfield", "MA", "01103", false, true);
Quick, what do the two booleans mean again?

So you can imagine that some classes can have even more fields, with a variety of types, and working with their constructors gets ridiculous. Fortunately, there is a design pattern that can help you out: the Builder pattern.

A builder is a class that builds another class. You use it instead of directly calling a constructor. Here is a builder example.
public class USAddressBuilder {
  final String streetAddressLine1;
  String streetAddressLine2 = null;
  final String city;
  final String state;
  String zipCode = null;
  boolean isPostOfficeBox = false;
  boolean isAptOrCondo = false;

  public USAddressBuilder (String a, String c, String s) {
    if (a == null) {
      throw new IllegalArgumentException ("null street address");
    }
    if (c == null) {
      throw new IllegalArgumentException ("null city");
    }
    if (s == null) {
      throw new IllegalArgumentException ("null state");
    }
    this.streetAddressLine1 = a;
    this.city = c;
    this.state = s;
  }
  public USAddressBuilder streetAddressLine2 (String a) {
    this.streetAddressLine2 = a; return this;
  }
  public USAddressBuilder zipCode (String z) {
    this.zipCode = z; return this;
  }
  public USAddressBuilder isPostOfficeBox (boolean p) {
    this.isPostOfficeBox = p; return this;
  }
  public USAddressBuilder isAptOrCondo (boolean ac) {
    this.isAptOrCondo = ac; return this;
  }

  public USAddress build() {
    return new USAddress (this);
  }
}
Let's tear this one down.
  • This builder has the same fields as the class it builds. Some of the fields—those that are required—are final, but the optional ones aren't. The optional ones even get default values.
  • The builder's constructor only takes in the fields that are required.
  • To set the optional fields, you call specific builder methods for them. These methods return the builder again, so you can chain calls together (see below).
  • The build() method constructs a USAddress object from the builder data. The constructor (not shown) simply copies the builder fields into the object.
Here's how the builder is used.
USAddress a =
    new USAddressBuilder ("1234 Elm St.", "Springfield", "MA")
    .streetAddressLine2 ("Apt. 56").zipCode ("01103")
    .isAptOrCondo (true).build();
This code is longer, but it's much easier to read. Other nice things you get:
  • You can leave out fields that aren't important (like isPostOfficeBox).
  • The logic for constructing the class is mostly moved over to the builder. This is nice for classes that have lots of other stuff in them.
  • The builder has the option of reusing objects that it already constructed. If you ask for an object with the same data, and those objects are immutable, you could just as well use a copy made earlier. This can save on memory usage. (For more, check out another design pattern, Flyweight.)
  • The builder has the option of sending back a subclass instance. Imagine an AddressBuilder that returns Address objects; if you use a builder and pass in US-style address information, the builder can send you back a specific subclass of Address that specializes in US address data.
There are some downsides.
  • You have to write a lot more code to implement this builder. It's a sacrifice you have to make for easier use later on.
  • It's another class. You can mitigate this downside a little by making the builder an inner class of what it builds (which is what I usually do).
  • Some libraries and frameworks can only use constructors for your classes. I see this more as a problem on their end and not a fault of this pattern, but it's a practical consideration. You might have to make allowances to work within the constraints imposed on you.

No comments:

Post a Comment