Benji's Blog

Builder Pattern with Java 8 Lambdas

The builder patten is often used to construct objects with many properties. It makes it easier to read initialisations by having parameters named at the callsite, while helping you only allow the construction of valid objects.

Builder implementations tend to either rely on the constructed object being mutable, and setting fields as you go, or on duplicating all the settable fields within the builder.

Since Java 8, I find myself frequently creating lightweight builders by defining an interface for each initialisation stage.

Let’s suppose we have a simple immutable Person type like

static class Person {
    public final String firstName;
    public final String lastName;
    public final Centimetres height;

    private Person(String firstName, String lastName, Centimetres height) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.height = height;
    }
}

I’d like to be able to construct it using a builder, so I can see at a glance which parameter is which.

Person benji = person()
    .firstName("benji")
    .lastName("weber")
    .height(centimeters(182));

All that is needed to support this now is 3 single-method interfaces to define each stage, and a method to create the builder

The three interfaces are as follows. Each has a single method, so is compatible with a lambda, and each method returns another single method interface. The final interface returns our completed Person type.

interface FirstNameBuilder {
    LastNameBuilder firstName(String firstName);
}
interface LastNameBuilder {
    HeightBuilder lastName(String lastName);
}
interface HeightBuilder {
    Person height(Centimetres height);
}

Now we can create a person() method which creates the builder using lambdas.

public static FirstNameBuilder person() {
    return firstName -> lastName -> height -> new Person(firstName, lastName, height);
}

While it is still quite verbose, this builder definition is barely longer than simply adding getters for each of the fields.

Suppose we wanted to be able the give people’s height in millimetres as well as centimetres. We could simply add a default method to the HeightBuilder interface that does the conversion.

interface HeightBuilder {
    Person height(Centimetres height);
    default Person height(MilliMetres millis) {
        return height(millis.toCentimetres());
    }
}

We can use the same approach to present different construction “paths” without making our interfaces incompatible with lambdas (which is necessary to keep it concise)

Let’s look at a more complex example of a “Burger” type. We wish to allow construction of burgers, but if the purchaser is a vegetarian we would like to restrict the available choices to only vegetarian options.

The simple meat-eater case looks exactly like above

Burger lunch = burger()
    .with(beef())
    .and(bacon());
class Burger {
    public final Patty patty;
    public final Topping topping;

    private Burger(Patty patty, Topping topping) {
        this.patty = patty;
        this.topping = topping;
    }

    public static BurgerBuilder burger() {
        return patty -> topping -> new Burger(patty, topping);
    }

    interface BurgerBuilder {
        ToppingBuilder with(Patty patty);
    }
    interface ToppingBuilder {
        Burger and(Topping topping);
    }
}

Now let’s introduce a vegetarian option. It will be a compile failure to put meat into a vegetarian burger.

Burger lunch = burger()
    .vegetarian()
    .with(mushroom())
    .and(cheese());

Burger failure = burger()
    .vegetarian()
    .with(beef()) // fails to compile. Beef is not vegetarian.
    .and(cheese());

To support this we add a default method to our BurgerBuilder which returns a new VegetarianBuilder which dissallows meat.

interface BurgerBuilder {
    ToppingBuilder with(Patty patty);
    default VegetarianBuilder vegetarian() {
        return patty -> topping -> new Burger(patty, topping);
    }
}
interface VegetarianBuilder {
    VegetarianToppingBuilder with(VegetarianPatty main);
}
interface VegetarianToppingBuilder {
    Burger and(VegetarianTopping topping);
}

After you have expressed your vegetarian preference, the builder will no longer present you with the option of choosing meat.

Now, let’s add the concept of free toppings. After choosing the main component of the burger we can choose to restrict ourselves to free toppings. In this example Tomato is free but Cheese is not. It will be a compile option to add cheese as a free topping. Now the divergent option is not the first in the chain.

Burger lunch = burger()
    .with(beef()).andFree().topping(tomato());

Burger failure = burger()
    .with(beef()).andFree().topping(cheese()); // fails to compile. Cheese is not free

We can support this by adding a new default method to our ToppingBuilder, which in turn calls the abstract method, meaning we don’t have to repeat the entire chain of lambdas required to construct the burger again.

interface ToppingBuilder {
    Burger and(Topping topping);
    default FreeToppingBuilder free() {
        return topping -> and(topping);
    }
}
interface FreeToppingBuilder {
    Burger topping(FreeTopping topping);
}

Here’s the full code from the burger example, with all the types involved.

class Burger {
    public final Patty patty;
    public final Topping topping;

    private Burger(Patty patty, Topping topping) {
        this.patty = patty;
        this.topping = topping;
    }

    public static BurgerBuilder burger() {
        return patty -> topping -> new Burger(patty, topping);
    }

    interface BurgerBuilder {
        ToppingBuilder with(Patty patty);
        default VegetarianBuilder vegetarian() {
            return patty -> topping -> new Burger(patty, topping);
        }
    }
    interface VegetarianBuilder {
        VegetarianToppingBuilder with(VegetarianPatty main);
    }
    interface VegetarianToppingBuilder {
        Burger and(VegetarianTopping topping);
    }
    interface ToppingBuilder {
        Burger and(Topping topping);
        default FreeToppingBuilder andFree() {
            return topping -> and(topping);
        }
    }
    interface FreeToppingBuilder {
        Burger topping(FreeTopping topping);
    }

}

interface Patty {}
interface BeefPatty extends Patty {
    public static BeefPatty beef() { return null;}
}
interface VegetarianPatty extends Patty, Vegetarian {}
interface Tofu extends VegetarianPatty {
    public static Tofu tofu() { return null; }
}
interface Mushroom extends VegetarianPatty {
    public static Mushroom mushroom() { return null; }
}

interface Topping {}
interface VegetarianTopping extends Vegetarian, Topping {}
interface FreeTopping extends Topping {}
interface Bacon extends Topping {
    public static Bacon bacon() { return null; }
}
interface Tomato extends VegetarianTopping, FreeTopping {
    public static Tomato tomato() { return null; }
}
interface Cheese extends VegetarianTopping {
    public static Cheese cheese() { return null; }
}

interface Omnivore extends Vegetarian {}
interface Vegetarian extends Vegan {}
interface Vegan extends DietaryChoice {}
interface DietaryChoice {}

When would(n’t) you use this?

Often a traditional builder just makes more sense.

If you want your builder to be used to supply an arbitrary number of fields in an arbitrary order then this isn’t for you.

This approach restricts the order in which fields are initialised to a specific order. This is can be a feature – sometimes it’s useful to ensure that some parameters are supplied first. e.g. to ensure mandatory parameters are supplied without the boilerplate of the typesafe builder pattern. It’s also easier to make the order flexible in the future if you need to than the other way around.

If your builder forms part of a public API then this probably isn’t for you.

Traditional builders are easier to change without breaking existing uses. This approach makes it easy to change uses with refactoring tools, provided you own all the affected code and can make changes to it. To change behaviour without breaking consumers in this approach you would have to restrict yourself to adding default methods rather than modifying existing interfaces.

On the other hand, by being restrictive in what it allows to compile, this does help you help people using your code to use it in the way you intended.

Where I do find myself using this approach is building lightweight fluent interfaces both to make the code more readable, and to help out my future self by letting the IDE autocomplete required field/code blocks. For instance, when recently implementing some automated performance tests, where we needed a warmup and a rampup period, we used one of these to prevent us from forgetting to include them.

When things are less verbose, you end up using them more often, in places you might not have bothered otherwise.