Skip to content

MagicRecordBuilders

poetix edited this page Apr 25, 2012 · 7 revisions

Builders Without Boilerplate

Suppose you have a record class - essentially a struct - like the following:

public class Person {
    public String name;
    public String nickname;
    public Date dateOfBirth;

    public static Builder builder() { ... }
}

or possibly the following (a conventional Java Bean):

public class Person {
    private String name;
    private String nickname;
    private Date dateOfBirth;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    [...other getters and setters...]

    public static Builder builder() { ... }
}

You also have a fluent interface for building Persons:

public interface Builder extends Supplier<Person> {
    Builder with_name(String name);
    Builder with_nickname(String nickname);
    Builder born_on(Date dateOfBirth);
}

which enables you to specify the properties of a new Person as follows:

Person person = Person.builder().with_name("Arthur Jackson")
                                .with_nickname("Two sheds")
                                .born_on(june(30).of(1963))
                                .get();

If we were to implement Builder manually, it might look something like this:

public class Builder implements Supplier<Person> {
    private String name;
    private String nickname;
    private Date dateOfBirth;

    public Builder with_name(String name) { this.name = name; }
    public Builder with_nickname(String nickname) { this.nickname = nickname; }
    public Builder born_on(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; }

    @Override public Person get() {
        Person person = new Person();
        person.name = name;
        person.nickname = nickname;
        person.dateOfBirth = dateOfBirth;
        return person;
    }
}

This is a bit tedious, as it means we have to write private fields to store each property of a Person, methods to update those fields, and one line of code per field to copy the stored values onto the newly-created Person object.

Stanislavski provides a way of getting an implementation of the Builder interface nearly for free:

public static Builder builder() {
    return MagicRecordBuilder.building(Person.class).using(Builder.class);
}

The only thing that needs to be done to make this work is to annotate the interface to help the MagicRecordBuilder associate the "born_on" method with the "dateOfBirth" property (it works out "name" and "nickname" automatically):

public interface Builder extends Supplier<Person> {
    Builder with_name(String name);
    Builder with_nickname(String nickname);
    @AddressesProperty("dateOfBirth") Builder born_on(Date dateOfBirth);
}

Immutable records

In general, it's a good idea to separate the "Builder" which enables you to build up the properties of a record dynamically, assigning them one at a time, from the "Record" itself which, once built, should ideally be immutable:

public class Person {
    public final String name;
    public final String nickName;
    public final Date dateOfBirth;

    public Person(String name, String nickName, Date dateOfBirth) {
        this.name = name;
        this.nickName = nickName;
        this.dateOfBirth = dateOfBirth;
    }

    public static Builder builder() { ... }
}

Here, we can't just new up an uninitialised record then set properties on it reflectively - we have to call the constructor with the right parameters.

MagicRecordBuilder will do the right thing provided that each of the constructor parameters is annotated with the name of the property it's assigned to:

public Person(@AssignedTo("name") String name,
              @AssignedTo("nickName") String nickName,
              @AssignedTo("dateOfBirth") Date dateOfBirth) {
    this.name = name;
    this.nickName = nickName;
    this.dateOfBirth = dateOfBirth;
}

(this is a bit tedious, but the best we can do in Java).

Alternatively, we can use a TypedKeywordArguments collection to construct the record:

public class Person {
    public static final TypedKeyword<Person, String> NAME = newTypedKeyword();
    public final String name;

    public static final TypedKeyword<Person, String> NICK_NAME = newTypedKeyword();
    public final String nickName;

    public static final TypedKeyword<Person, Date> DATE_OF_BIRTH = newTypedKeyword();
    public final Date dateOfBirth;

    public final TypedKeywordArguments<Person> properties;

    public <T> getProperty(TypedKeyword<Person, T> keyword) {
        return keyword.from(properties);
    }

    public Person(TypedKeywordArguments<Person> args) {
        this.name = NAME.from(args);
        this.nickName = NICK_NAME.from(args);
        this.dateOfBirth = DATE_OF_BIRTH.from(args);
        this.properties = properties;
    }

    public static Builder builder() { ... }
}

This has the advantage (which you may not need) that individual properties of a person can be read dynamically in a type-safe manner using keywords:

String name = person.getProperty(Person.NAME);

We can also export the entire property set from one record to another, possibly modifying it as we go:

Person clone = new Person(oldPerson.properties.with(Person.NICKNAME.of("Two Jags"));

This provides a reasonably intuitive pattern for "applying" a set of updates to the properties of one immutable record to obtain a new immutable record.

Clone this wiki locally