Practical builder design patterns in Java

Although none of this blog post contains Turing Award material, we will describe a practical way to use the builder pattern in Java that reduces bugs and makes the code easier to follow.

Audience requirements: Java

According to Wikipedia, “The intent of the Builder design pattern is to separate the construction of a complex object from its representation.

Benefits

We like the following benefits of builders, in particular:

No arbitrary constructor parameter ordering

This is not a problem if there is a well-accepted ordering convention, e.g.

Vector3d(double x, double y, double z)

However, in the code below, one can accidentally pass in arguments in the wrong order and cause bugs:

new RealizedTax(Money shortTerm, Money longTerm)

Clearer names

Even in those cases where the compiler will prevent you from reordering the constructor parameters, it helps to have a clear name for each parameter, especially when the type of the parameter is not sufficient. For example, it may not be obvious below that 252 means “number of market days”:

new TotalReturn(onesBasedReturn(1.02), 252)

Our Java interface for builders

Every builder class in our codebase implements RBBuilder. 

We admittedly do not use the Java interface mechanism for its intended purpose here. The RBBuilder interface mostly exists to enforce a convention and to simplify some code. In general, interfaces are useful when polymorphism is involved. However, it almost never makes sense for a method to take as an argument a builder of some supertype; a builder is almost always used in-line as a more legible replacement for a traditional constructor.

The code below had comments removed; we will add explanations to the blog post instead.

Note: Our codebase is in Java 8, which allows default interface methods.

public interface RBBuilder<T> {

  void sanityCheckContents();

  default T build() {
    sanityCheckContents();
    return buildWithoutPreconditions();
  }

  default T checkNotAlreadySet(T currentFieldValue, T newFieldValue) {
    Preconditions.checkArgument(
        currentFieldValue == null,
        "You are trying to set a value twice in a builder, which is probably a bug: from %s to %s",
        currentFieldValue, newFieldValue);
    return newFieldValue;
  }

  default T checkAlreadySet(T currentFieldValue, T newFieldValue) {
    Preconditions.checkArgument(
        currentFieldValue != null,
        "You are trying to reset a value to %s in a builder, but the value has not already been set",
        newFieldValue);
    return newFieldValue;
  }

  @VisibleForTesting
  T buildWithoutPreconditions();

}

We will explain the methods one at a time.

#sanityCheckContents

This should throw an exception if an object cannot be built for some reason, such as:

  • we forgot to specify a value for one or more of the fields.
  • some data inconsistency exists between 2 or more fields – e.g. if long-term tax rate are larger than short-term but the code disallows that. If the inconsistency applies only to a single field, we prefer to perform the check directly in the builder’s setter for that field.

We rarely call #sanityCheckContents directly; it is only used in #build.

#build

Builds the object, after confirming that it is valid. We never need to override this.

#checkNotAlreadySet

This convenience method throws an exception if a field is given a value more than once. This is almost always an error. As a convention, the setter methods in a builder start with ‘set’, and the assumption is that they do not get called more than once.

Example:

public CapitalGainsTaxRatesBuilder setMarginalLongTermFederal(TaxRate marginalLongTermFederal) {
  this.marginalLongTermFederal = checkNotAlreadySet(this.marginalLongTermFederal, marginalLongTermFederal);
  return this;
}

#checkAlreadySet

There are a few cases where we do want to be able to set a value in a builder more than once; for instance, to overwrite a default value. In those cases, we use checkAlreadySet to confirm that we are indeed overwriting a field’s value, and specifically not giving it its first value. For code clarity, we have a convention that the names of such setter methods must start with ‘reset’ instead of ‘set’.

public AllComparableBacktestSettingsBuilder resetTaxSituation(TaxSituation taxSituation) {
  this.taxSituation = checkAlreadySet(this.taxSituation, taxSituation);
  return this;
}

#buildWithoutPreconditions

This builds an object, but without calling #sanityCheckContents. Here’s why it exists:

  • Allows partial objects: Our TaxSituation class stores several tax rates (short- vs long-term, federal vs. state, qualified dividend). If some non-test code only looks at the dividend rate, its corresponding unit test would be simpler if its TaxSituation input object only specifies the dividend rate. Needing to specify a full object in tests is an even bigger problem in practice, because most objects contain other objects, and things can get out of hand quickly.
  • Software engineering: Sanity checking is a different concept than construction; separating them makes for cleaner code. It also allows you to check whether it’s possible to construct an object without actually attempting to construct it (although we do not do that in practice).

Note: The @VisibleForTesting annotation (from Guava) can be used to enforce that buildWithoutPreconditions only gets called from test code. This makes sense; in production, we should never be skipping the preconditions.

Sample usage:

public class CapitalGainsTaxRates {

  private final TaxRate marginalLongTermFederal;
  private final TaxRate marginalOrdinaryIncome;
  private final TaxRate marginalLongTermState;
  private final TaxRate marginalShortTermState;

  private CapitalGainsTaxRates(TaxRate marginalLongTermFederal, TaxRate marginalOrdinaryIncome,
                               TaxRate marginalLongTermState, TaxRate marginalShortTermState) {
    this.marginalLongTermFederal = marginalLongTermFederal;
    this.marginalOrdinaryIncome = marginalOrdinaryIncome;
    this.marginalLongTermState = marginalLongTermState;
    this.marginalShortTermState = marginalShortTermState;
  }

  public TaxRate getMarginalLongTermFederal() {
    return marginalLongTermFederal;
  }

  public TaxRate getMarginalOrdinaryIncome() {
    return marginalOrdinaryIncome;
  }

  public TaxRate getMarginalLongTermState() {
    return marginalLongTermState;
  }

  public TaxRate getMarginalShortTermState() {
    return marginalShortTermState;
  }

  @Override
  public String toString() {
    return Strings.format("[CPR Fed (LT, ST)= %s %s ; ST = %s %s CPR]",
        marginalLongTermFederal, marginalOrdinaryIncome, marginalShortTermState, marginalLongTermState);
  }
public static class CapitalGainsTaxRatesBuilder implements RBBuilder<CapitalGainsTaxRates> {

    private TaxRate marginalLongTermFederal;
    private TaxRate marginalOrdinaryIncome;
    private TaxRate marginalLongTermState;
    private TaxRate marginalShortTermState;

    private CapitalGainsTaxRatesBuilder() {}

    public static CapitalGainsTaxRatesBuilder capitalGainsTaxRatesBuilder() {
      return new CapitalGainsTaxRatesBuilder();
    }

    public CapitalGainsTaxRatesBuilder setMarginalLongTermFederal(TaxRate marginalLongTermFederal) {
      this.marginalLongTermFederal = checkNotAlreadySet(this.marginalLongTermFederal, marginalLongTermFederal);
      return this;
    }

    public CapitalGainsTaxRatesBuilder setMarginalOrdinaryIncome(TaxRate marginalOrdinaryIncome) {
      this.marginalOrdinaryIncome = checkNotAlreadySet(this.marginalOrdinaryIncome, marginalOrdinaryIncome);
      return this;
    }

    public CapitalGainsTaxRatesBuilder setMarginalLongTermState(TaxRate marginalLongTermState) {
      this.marginalLongTermState = checkNotAlreadySet(this.marginalLongTermState, marginalLongTermState);
      return this;
    }

    public CapitalGainsTaxRatesBuilder setMarginalShortTermState(TaxRate marginalShortTermState) {
      this.marginalShortTermState = checkNotAlreadySet(this.marginalShortTermState, marginalShortTermState);
      return this;
    }

    @Override
    public void sanityCheckContents() {
      Preconditions.checkNotNull(marginalLongTermFederal);
      Preconditions.checkNotNull(marginalOrdinaryIncome);
      Preconditions.checkNotNull(marginalLongTermState);
      Preconditions.checkNotNull(marginalShortTermState);
    }

    @Override
    public CapitalGainsTaxRates buildWithoutPreconditions() {
      return new CapitalGainsTaxRates(
          marginalLongTermFederal, marginalOrdinaryIncome,
          marginalLongTermState, marginalShortTermState);
    }

  }

}

A few notes:

  1. The data class (the one built by the builder) has a private constructor (line 8). This prevents any instantiation other than through the builder.
  2. The builder also has a private constructor (line 45), but this is only because we have a convention of always using static constructor methods for object construction. This is for reasons unrelated to this post, so you can ignore it.

The builder does make the data class code longer, but object construction everywhere else will now be clear and error-proof. Example:

public static final CapitalGainsTaxRates CAPITAL_GAINS_TAX_RATES_2016_HIGHEST = capitalGainsTaxRatesBuilder()
    .setMarginalLongTermFederal(taxRateInPercent(20.0 + 3.8))
    .setMarginalLongTermState(taxRateInPercent(13.3))
    .setMarginalShortTermState(taxRateInPercent(13.3))
    .setMarginalOrdinaryIncome(taxRateInPercent(39.6))
    .build();

Although wordier, that was much clearer than relying on the order of constructor arguments (which doesn’t clarify which rate means what) as in:

new CapitalGainsTaxRates(
  taxRateInPercent(20.0 + 3.8),
  taxRateInPercent(13.3),
  taxRateInPercent(13.3),
  taxRateInPercent(39.6))

Summary

The builder design pattern is very useful. We use builders a lot in our codebase; it enhances code legibility and reduces bugs. We created a common Java interface for our builders, which streamlines the construction of partial objects for unit tests, and (secondarily) keeps the code cleaner by enforcing some conventions at compile time.

Author: Rowboat Advisors

Rowboat Advisors builds software for sophisticated and fully automated portfolio management for the financial advisor industry.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s