Skip to content

Latest commit

 

History

History
249 lines (184 loc) · 6.69 KB

File metadata and controls

249 lines (184 loc) · 6.69 KB

zerobuilder for value types

Motivation

Immutable types, also known as values, are increasingly used in place of traditional java beans. Their constructors often have lots of arguments. This can lead to code that's hard to read.

Using the standard builder pattern may improve readability, but there are also some drawbacks:

  • Creating the builder class is tedious, and the code has to be kept in sync.
  • The builder pattern makes it possible to "forget" a constructor argument. For instance, extending the argument list of a constructor should cause compile errors elsewhere.

Zerobuilder takes care of the boilerplate by generating two different variants of the builder pattern:

  • A Builder that fails at compile time if constructor arguments are missing.
  • A traditional-style Updater to make and modify shallow copies.

Builder

Start by adding the @Builder annotation to a constructor:

final class Message {

  final String sender;
  final String body;
  final String recipient;

  @Builder
  Message(String sender, String body, String recipient) {
    this.sender = sender;
    this.body = body;
    this.recipient = recipient;
  }
}

The following class will be generated:

@Generated final class MessageBuilders {

  static MessageBuilder.Sender messageBuilder() { ... }

  static final class MessageBuilder {
    interface Sender { Body sender(String sender); }
    interface Body { Recipient body(String body); }
    interface Recipient { Message recipient(String recipient); }
  }
}

The messageBuilder method returns MessageBuilder.Sender, the first "step" in a chain of interfaces towards the constructor goal:

steps

Changing step order

By default, the steps that are generated by @Builder (see image above) are in the original order of the goal arguments. If for some reason you would like to call them in a different order, you can use the @Step annotation:

  @Builder
  Message(@Step(1) String sender,
          @Step(2) String body,
          @Step(0) String recipient) {
    this.sender = sender;
    this.body = body;
    this.recipient = recipient;
  }

Updater

The @Updater annotation can be used instead of, or in addition to, the @Builder annotation:

final class Message {

  final String sender;
  final String body;
  final String recipient;

  @Updater
  Message(String sender, String body, String recipient) {
    this.sender = sender;
    this.body = body;
    this.recipient = recipient;
  }
}

The @Updater annotation will scan the surrounding class and raise a compile error, if there doesn't exist a corresponding projection for each goal parameter. This is the case in Message.java. In this case, the projections are the non-private final fields sender, body and recipient.

The following class will be generated:

@Generated final class MessageBuilders {

  static MessageUpdater messageUpdater(Message message) { ... }

  static final class MessageUpdater {
    MessageUpdater sender(String sender) { ... }
    MessageUpdater body(String body) { ... }
    MessageUpdater recipient(String recipient) { ... }
    Message done() { ... }
  }
}

What's a projection?

This can either be a non-private field or a non-private, non-static method, with name and type matching one of the goal parameters.

For example, any of the following would be a valid projection for a constructor parameter String sender:

  1. A non-private, non-static field String sender
  2. A non-private, non-static method String sender()
  3. A non-private, non-static method String getSender()

The projections are found in this order.

For example, if the field String sender exists and is not private, then it will be used to initialize the updater. Even if the methods String sender() or String getSender() exist, they will be ignored.

A projection method may be abstract. Fields and methods that are inherited from an ancestor class are treated as if they were defined directly.

Recycling

The generated code can be made more efficient by adding a @Recycle annotation:

@Recycle
@Builder
@Updater
Doo(String foo) {
  this.foo = foo;
}

With @Recycle, the generated code will attempt to reuse builder and updater instances. Please note that this uses ThreadLocal, which may cause problems under certain conditions.

Recycling / Type parameters

The @Recycle annotation is not allowed and will raise a compile error, if Doo has any type parameters:

class Doo<E> {
  final E e;
  @Recycle // Error
  @Builder
  Doo(E e) { this.e = e; }
}

Recycling / Release

Even with recycling, the dooBuilder and dooUpdater methods will not always return the same object:

final class Doo {
  final String foo;
  @Recycle @Builder @Updater
  Doo(String foo) {
    this.foo = foo;
  }

  public static void main(String[] args) {
    DooBuilders.DooBuilder.Foo builder = dooBuilder();
    DooBuilders.DooUpdater updater = dooUpdater(builder.foo(""));
    Doo doo = updater.done();
    updater == dooUpdater(doo);         // true
    builder == dooBuilder();            // true
    updater == dooUpdater(doo);         // false
    builder == dooBuilder();            // false
    dooBuilder() == dooBuilder();       // false
    dooUpdater(doo) == dooUpdater(doo); // false
  }
}

Updater instances will only be reused after done() is invoked, and builder instances will only be reused after the final step is invoked.

Access level

By default, the generated static methods fooBuilder and fooUpdater are public. This can be changed to package visibility by adding an @AccessLevel annotation.

@AccessLevel(Access.PACKAGE)
@Builder
public Doo(String foo) {
  this.foo = foo;
}

auto-value

Use @Builder and optionally @Updater on the factory method:

import com.google.auto.value.AutoValue;
import net.zerobuilder.Builder;
import net.zerobuilder.Updater;

@AutoValue
abstract class Animal {

  @Builder
  @Updater
  static Animal create(String name, int numberOfLegs) {
    return new AutoValue_Animal(name, numberOfLegs);
  }

  abstract String name();
  abstract int numberOfLegs();
}

The following convenience methods could then also be added to Animal:

static AnimalBuilders.AnimalBuilder.Name builder() {
  return AnimalBuilders.animalBuilder();
}

AnimalBuilders.AnimalUpdater updater() {
  return AnimalBuilders.animalUpdater(this);
}