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
Builderthat fails at compile time if constructor arguments are missing. - A traditional-style
Updaterto make and modify shallow copies.
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:
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;
}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() { ... }
}
}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:
- A non-private, non-static field
String sender - A non-private, non-static method
String sender() - 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.
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.
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; }
}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.
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;
}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);
}