Saturday, June 18, 2016

Creating Value Objects with Immutables

In response to my recent post AutoValue: Generated Immutable Value Classes, Brandon suggested that it might be interesting to see how AutoValue compares to Project Lombok and Immutables and Kevin seconded this. I agree that this is a good idea, but I am first publishing this post as a brief overview of Immutables because I have already provided similar posts for Lombok and AutoValue.

Immutables 2.2.5 is available from the Maven Central Repository and its license page states "The Immutables toolkit and all required dependencies are covered under The Apache Software License, Version 2.0." The Get started! page states that "Java 7 or higher is required to run the Immutables annotation processor."

Immutables, like AutoValue, uses compile-time annotations to generate the source code for the classes that define immutable objects. Because they both use this approach, both introduce only compile-time dependencies and their respective JARs are not needed on the application's runtime classpath. In other words, the Immutable JARs need to be on the compiler's (javac's) classpath but not on Java launcher's (java's) classpath.

The code listing for a "template" Person class is shown in the next code listing (Person.java). It looks very similar to the Person.java I used in my AutoValue demonstration.

Person.java
package dustin.examples.immutables;

import org.immutables.value.Value;

/**
 * Represents an individual as part of demonstration of
 * the Immutables project (http://immutables.github.io/).
 */
@Value.Immutable  // concrete extension will be generated by Immutables
abstract class Person
{
   /**
    * Provide Person's last name.
    *
    * @return Last name of person.
    */
   abstract String lastName();

   /**
    * Provide Person's first name.
    *
    * @return First name of person.
    */
   abstract String firstName();

   /**
    * Provide Person's birth year.
    *
    * @return Person's birth year.
    */
   abstract long birthYear();
}

The only differences in this "template" class and the "template" class I listed in my AutoValue post is the name of the package, the Javadoc comments on which product is being demonstrated, and (most significantly) the annotation imported and applied to the class. There is a specific "create" method in the AutoValue example that's not in the Immutables example, but that's only because I didn't demonstrate use of AutoValue's builder, which would have rendered the "create" method unnecessary.

When I appropriately specify use of Immutables on my classpath and use javac to compile the above source code, the annotation processor is invoked and the following Java source code is generated:

ImmutablePerson.java
package dustin.examples.immutables;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.annotation.Generated;

/**
 * Immutable implementation of {@link Person}.
 * <p>
 * Use the builder to create immutable instances:
 * {@code ImmutablePerson.builder()}.
 */
@SuppressWarnings("all")
@Generated({"Immutables.generator", "Person"})
final class ImmutablePerson extends Person {
  private final String lastName;
  private final String firstName;
  private final long birthYear;

  private ImmutablePerson(String lastName, String firstName, long birthYear) {
    this.lastName = lastName;
    this.firstName = firstName;
    this.birthYear = birthYear;
  }

  /**
   * @return The value of the {@code lastName} attribute
   */
  @Override
  String lastName() {
    return lastName;
  }

  /**
   * @return The value of the {@code firstName} attribute
   */
  @Override
  String firstName() {
    return firstName;
  }

  /**
   * @return The value of the {@code birthYear} attribute
   */
  @Override
  long birthYear() {
    return birthYear;
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Person#lastName() lastName} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param lastName A new value for lastName
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withLastName(String lastName) {
    if (this.lastName.equals(lastName)) return this;
    String newValue = Objects.requireNonNull(lastName, "lastName");
    return new ImmutablePerson(newValue, this.firstName, this.birthYear);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Person#firstName() firstName} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param firstName A new value for firstName
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withFirstName(String firstName) {
    if (this.firstName.equals(firstName)) return this;
    String newValue = Objects.requireNonNull(firstName, "firstName");
    return new ImmutablePerson(this.lastName, newValue, this.birthYear);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Person#birthYear() birthYear} attribute.
   * A value equality check is used to prevent copying of the same value by returning {@code this}.
   * @param birthYear A new value for birthYear
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withBirthYear(long birthYear) {
    if (this.birthYear == birthYear) return this;
    return new ImmutablePerson(this.lastName, this.firstName, birthYear);
  }

  /**
   * This instance is equal to all instances of {@code ImmutablePerson} that have equal attribute values.
   * @return {@code true} if {@code this} is equal to {@code another} instance
   */
  @Override
  public boolean equals(Object another) {
    if (this == another) return true;
    return another instanceof ImmutablePerson
        && equalTo((ImmutablePerson) another);
  }

  private boolean equalTo(ImmutablePerson another) {
    return lastName.equals(another.lastName)
        && firstName.equals(another.firstName)
        && birthYear == another.birthYear;
  }

  /**
   * Computes a hash code from attributes: {@code lastName}, {@code firstName}, {@code birthYear}.
   * @return hashCode value
   */
  @Override
  public int hashCode() {
    int h = 31;
    h = h * 17 + lastName.hashCode();
    h = h * 17 + firstName.hashCode();
    h = h * 17 + Long.hashCode(birthYear);
    return h;
  }

  /**
   * Prints the immutable value {@code Person} with attribute values.
   * @return A string representation of the value
   */
  @Override
  public String toString() {
    return "Person{"
        + "lastName=" + lastName
        + ", firstName=" + firstName
        + ", birthYear=" + birthYear
        + "}";
  }

  /**
   * Creates an immutable copy of a {@link Person} value.
   * Uses accessors to get values to initialize the new immutable instance.
   * If an instance is already immutable, it is returned as is.
   * @param instance The instance to copy
   * @return A copied immutable Person instance
   */
  public static ImmutablePerson copyOf(Person instance) {
    if (instance instanceof ImmutablePerson) {
      return (ImmutablePerson) instance;
    }
    return ImmutablePerson.builder()
        .from(instance)
        .build();
  }

  /**
   * Creates a builder for {@link ImmutablePerson ImmutablePerson}.
   * @return A new ImmutablePerson builder
   */
  public static ImmutablePerson.Builder builder() {
    return new ImmutablePerson.Builder();
  }

  /**
   * Builds instances of type {@link ImmutablePerson ImmutablePerson}.
   * Initialize attributes and then invoke the {@link #build()} method to create an
   * immutable instance.
   * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
   * but instead used immediately to create instances.</em>
   */
  static final class Builder {
    private static final long INIT_BIT_LAST_NAME = 0x1L;
    private static final long INIT_BIT_FIRST_NAME = 0x2L;
    private static final long INIT_BIT_BIRTH_YEAR = 0x4L;
    private long initBits = 0x7L;

    private String lastName;
    private String firstName;
    private long birthYear;

    private Builder() {
    }

    /**
     * Fill a builder with attribute values from the provided {@code Person} instance.
     * Regular attribute values will be replaced with those from the given instance.
     * Absent optional values will not replace present values.
     * @param instance The instance from which to copy values
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder from(Person instance) {
      Objects.requireNonNull(instance, "instance");
      lastName(instance.lastName());
      firstName(instance.firstName());
      birthYear(instance.birthYear());
      return this;
    }

    /**
     * Initializes the value for the {@link Person#lastName() lastName} attribute.
     * @param lastName The value for lastName 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder lastName(String lastName) {
      this.lastName = Objects.requireNonNull(lastName, "lastName");
      initBits &= ~INIT_BIT_LAST_NAME;
      return this;
    }

    /**
     * Initializes the value for the {@link Person#firstName() firstName} attribute.
     * @param firstName The value for firstName 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder firstName(String firstName) {
      this.firstName = Objects.requireNonNull(firstName, "firstName");
      initBits &= ~INIT_BIT_FIRST_NAME;
      return this;
    }

    /**
     * Initializes the value for the {@link Person#birthYear() birthYear} attribute.
     * @param birthYear The value for birthYear 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder birthYear(long birthYear) {
      this.birthYear = birthYear;
      initBits &= ~INIT_BIT_BIRTH_YEAR;
      return this;
    }

    /**
     * Builds a new {@link ImmutablePerson ImmutablePerson}.
     * @return An immutable instance of Person
     * @throws java.lang.IllegalStateException if any required attributes are missing
     */
    public ImmutablePerson build() {
      if (initBits != 0) {
        throw new IllegalStateException(formatRequiredAttributesMessage());
      }
      return new ImmutablePerson(lastName, firstName, birthYear);
    }

    private String formatRequiredAttributesMessage() {
      List<String> attributes = new ArrayList<String>();
      if ((initBits & INIT_BIT_LAST_NAME) != 0) attributes.add("lastName");
      if ((initBits & INIT_BIT_FIRST_NAME) != 0) attributes.add("firstName");
      if ((initBits & INIT_BIT_BIRTH_YEAR) != 0) attributes.add("birthYear");
      return "Cannot build Person, some of required attributes are not set " + attributes;
    }
  }
}

Several observations can be made from examining the generated code (and you'll find that these are remarkably similar to the observations listed for AutoValue in my earlier post):

  • The generated class extends (implementation inheritance) the abstract class that was hand-written, allowing consuming code to use the hand-written class's API without having to know that a generated class was being used.
  • Fields were generated even though no fields were defined directly in the source class; Immutables interpreted the fields from the provided abstract accessor methods.
  • The generated class does not provide "set"/mutator methods for the fields (get/accessor methods). This is not surprising because a key concept of Value Objects is that they are immutable and even the name of this project (Immutables) implies this characteristic. Note that Immutables does provide some ability for modifiable objects with the @Value.Modifiable annotation.
  • Implementations of equals(Object), hashCode(), and toString() are automatically generated appropriately for each field with its type in mind.
  • Javadoc comments on the source class and methods are not reproduced on the generated extension class. Instead, simpler (and more generic) Javadoc comments are supplied on the generated class's methods and more significant (but still generic) Javadoc comments are provided on the builder class's methods.

As I stated with regards to AutoValue, one of the major advantages of using an approach such as Immutables generation is that developers can focus on the easier higher level concepts of what a particular class should support and the code generation ensures that the lower-level details are implemented consistently and correctly. However, there are some things to keep in mind when using this approach.

  • Immutables is most likely to be helpful when the developers are disciplined enough to review and maintain the abstract "source" Java class instead of the generated class.
    • Changes to the generated classes would be overwritten the next time the annotation processing generated the class again or generation of that class would have to be halted so that this did not happen.
    • The "template" abstract class has the documentation and other higher-level items most developers will want to focus on and the generated class simply implements the nitty gritty details.
  • You'll want to set your build/IDE up so that the generated classes are considered "source code" so that the abstract class will compile and any dependencies on the generated classes will compile.
  • Special care must be taken when using mutable fields with Immutables if one wants to maintain immutability (which is typically the case when choosing to use Immutables or Value Objects in general).

Conclusion

My conclusion can be almost word-for-word the same as for my post on AutoValue. Immutables allows developers to write more concise code that focuses on high-level details and delegates the tedious implementation of low-level (and often error-prone) details to Immutables for automatic code generation. This is similar to what an IDE's source code generation can do, but Immutables's advantage over the IDE approach is that Immutables can regenerate the source code every time the code is compiled, keeping the generated code current. This advantage of Immutables is also a good example of the power of Java custom annotation processing.

No comments: