Jilt 1.6 supports Functional Builders
Jilt, my Java library for automatically generating Builder pattern classes, has been garnering quite a bit of attention lately, as I’ve written about in two recent articles on this blog.
A couple of months ago, Thomas Schuehly opened an issue about adding support for Functional Builders to Jilt. Functional Builders are an alternative to the classic Java Builder pattern, first introduced by Guillaume Laforge (who is probably best known as the creator of the Groovy programming language) in an article on his blog. Thomas suggested adding Functional Builders as the fourth supported Builder style to Jilt (with the first three being Classic, Staged, and ‘Staged preserving order’).
I thought this would be a good fit for the library, and so, after a bit of tinkering,
the newest Jilt release, 1.6
, includes support for Functional Builders!
Since this is a relatively new pattern (the aforementioned blog article by Guillaume is only from January this year!), let me first quickly describe the basics of how it works (check out Guillaume’s article for a full explanation), and then show how we had to adapt it to fit the constraints Jilt operates under.
The Functional Builder pattern
Functional Builders look quite different from classic Builders.
There is no separate instance that has setter method calls chained on it,
and no final call to the build()
method at the end that returns an instance of the target class.
Functional Builders can be thought of as a way to implement the
named parameters
programming language feature, which Java lacks.
Instead of a separate Builder, the constructor of the built class will be used to create an instance of it.
But, the property values will not be passed to that constructor directly;
instead, the argument to the constructor is
variadic,
and its type is an interface nested inside the built class that has a single method,
returning void
, and taking the built class as the only parameter
(kind of like a Java 8 Consumer
,
with the target class as the value of the generic type parameter).
To make things less abstract, let’s use a concrete example:
public final class FullName {
public static interface FullNameConsumer {
public void accept(FullName fullName);
}
public FullName(FullNameConsumer... consumers) {
for (FullNameConsumer consumer : consumers) {
consumer.accept(this);
}
// any validations and other logic go here...
}
// ...
}
So, where do we get implementations of this nested interface?
The built class will have static methods that return instances of this interface.
There will be a static method for each property of the built class,
with the same name as the property.
The static method will take one argument,
of the same type that the corresponding property has.
The returned instance of the nested interface will,
in its accept
method, set that property on the instance passed to it to the value of the static method argument.
Continuing our above example, it looks something like this:
public final class FullName {
private String firstName, middleName, lastName;
public static FullNameConsumer firstName(final String firstName) {
return new FullNameConsumer() {
public void accept(FullName fullName) {
fullName.firstName = firstName;
}
};
}
public static FullNameConsumer middleName(String middleName) {
// we can use Java 8 lambda expressions
return fullName -> {
fullName.middleName = middleName;
};
}
public static FullNameConsumer lastName(String lastName) {
// since the lambda is a single statement,
// you can skip the braces
return fullName -> fullName.lastName = lastName;
}
// ...
}
Given all that, creating an instance of the target class is done through invoking its constructor, and passing it the results of calling the static methods with the values of the properties you want to set, such as:
FullName jfk = new FullName(
FullName.firstName("John"),
FullName.middleName("F"),
FullName.lastName("Kennedy")
);
// jfk.firstName == "John"
// jfk.middleName == "F"
// jfk.lastName == "Kennedy"
And, using Java’s static imports, we can approximate the named parameters language feature:
import static mypackage.FullName.firstName;
import static mypackage.FullName.lastName;
import static mypackage.FullName.middleName;
FullName jfk = new FullName(
firstName("John"),
middleName("F"),
lastName("Kennedy")
);
Notice that, although the nested interface must be public
(since it’s used as a parameter of the public constructor),
so in theory can be implemented outside the target class,
as long as the fields of the target class are private
,
only implementations from the static methods on the target class can modify those fields,
so you don’t have to worry about other implementations potentially messing with your class’s internals.
Functional Builders in Jilt
To add this feature to Jilt, and allow automatically generating classes that implement this pattern, we need to modify it slightly to adapt it to the constraints that Jilt operates under:
- As I wrote before, Jilt cannot modify hand-written classes, only generate new ones. So, we cannot generate the constructor in the target class, or the interface nested in it, or the static methods that return implementations of that interface.
- The above pattern treats all properties as optional. The differentiation between optional and required properties is a staple of Jilt, and we would like to retain it for Functional Builders as well.
- Since we can’t modify the target class by generating the static methods inside of it, that means the above point, about the fields of the target class being accessible only in methods inside it, no longer holds, so we have to find a solution to that problem as well.
I reached out to Guillaume on Twitter, and he was gracious enough to join the discussion in the issue. After some back and forth, we settled on the following design:
- We generate a static factory method on the Builder class, similarly like we do for the other Builders in Jilt. However, in order to keep the spirit of the pattern, we change the return type of the static factory method for Functional Builders to be the built class directly, and not the Builder class.
- The interfaces for the properties are generated inside a separate top-level interface, not in the Builder class, same as for existing Staged Builders in Jilt.
- Each required property has its own interface, while all optional properties share the same interface. The static factory method takes those required property interfaces as arguments first, in the same order as they were declared in the target class, and then the optional properties are the last, variadic, argument, which means they can be skipped, or provided in any order.
- The generated interfaces take an instance of a Builder in their
accept
methods. The Builder has a field for each property, with the same name, that isprivate
, and the Builder doesn’t include any setter methods. - We generate the per-property static methods that return the interface instances also on the Builder class.
This ensures these methods are the only ones that can mutate the Builder –
any implementations outside the Builder won’t have access to the
private
fields, and thus cannot change the instance passed to them in any way. - For the optional properties, since they share the same interface,
we generate the static methods for them under an additional nested class of the Builder, called
Optional
, to make them more discoverable.
Since we moved all field mutations into the Builder,
we can safely make the target class completely immutable,
including making the fields final
,
or using Java 14+ Records.
In order to use this new style of Builder in Jilt,
you need to set the style
attribute of the @Builder
annotation to the new BuilderStyle.FUNCTIONAL
value:
import org.jilt.Builder;
import org.jilt.BuilderStyle;
import org.jilt.Opt;
@Builder(style = BuilderStyle.FUNCTIONAL)
public final class FullName {
public final String firstName;
@Opt public final String middleName;
public final String lastName;
public FullName(String firstName,
String middleName, String lastName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
}
}
This will generate a Builder that can be used like this:
FullName jfk = FullNameBuilder.fullName(
FullNameBuilder.firstName("John"), // required here
FullNameBuilder.lastName("Kennedy"), // required here
FullNameBuilder.Optional.middleName("F") // could be skipped
);
And with static imports:
import static mypackage.FullNameBuilder.firstName;
import static mypackage.FullNameBuilder.lastName;
import static mypackage.FullNameBuilder.Optional.middleName;
FullName jfk = FullNameBuilder.fullName(
firstName("John"), // required here
lastName("Kennedy"), // required here
middleName("F") // could be skipped
);
The first two arguments passed to FullNameBuilder.fullName()
,
for firstName
and lastName
,
have to be present, since they correspond to required properties –
however, the third argument, for middleName
,
is an optional property (the field was annotated with the @Opt
annotation from Jilt),
so could have been skipped.
The FullName
class could also be re-written to use Records:
import org.jilt.Builder;
import org.jilt.BuilderStyle;
import org.jilt.Opt;
@Builder(style = BuilderStyle.FUNCTIONAL)
public record FullName(String firstName,
@Opt String middleName, String lastName) {
}
This generates a Builder class that can be used in the same way as when FullName
was a class.
Functional toBuilder
In the previous article,
I described a new feature in Jilt version 1.5
–
the toBuilder
attribute of the @Builder
annotation.
The idea behind it is that you can now generate a Builder class with a static toBuilder()
method that takes an instance of the target class,
and returns an instance of the Builder,
initialized with values copied from the instance of the target class passed as the argument.
This is useful for creating a copy of an instance of the target class with just a few properties changed,
while still making the target class completely immutable.
However, since the Functional style doesn’t use a Builder instance directly,
the way toBuilder
works for this variant is different.
In the Functional style, the toBuilder
method directly returns an instance of the target class,
similarly to the static factory method.
In order to modify the returned instance,
the toBuilder
method has a second, variadic argument that is the base type for all the generated per-property nested interfaces
(both for required and optional properties).
This way, we can pass any number of interface instances to toBuilder()
–
since the Builder instance will be initialized from an instance of the target class,
we consider all properties optional in this case.
So, continuing the FullName
example from above:
import org.jilt.Builder;
import org.jilt.BuilderStyle;
import org.jilt.Opt;
@Builder(style = BuilderStyle.FUNCTIONAL, toBuilder = "copy")
public final class FullName {
public final String firstName;
@Opt public final String middleName;
public final String lastName;
public FullName(String firstName,
String middleName, String lastName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
}
}
The toBuilder()
method can be used as follows:
import static mypackage.FullNameBuilder.firstName;
import static mypackage.FullNameBuilder.lastName;
import static mypackage.FullNameBuilder.Optional.middleName;
FullName jfk = FullNameBuilder.fullName(
firstName("John"),
lastName("Kennedy"),
middleName("F")
);
FullName rfk = FullNameBuilder.copy(jfk,
firstName("Robert")
);
// rfk.firstName == "Robert"
// rfk.middleName == "F"
// rfk.lastName == "Kennedy"
You can also add a hand-written toBuilder
instance method to your target class,
that delegates to the static one on the Builder:
public final class FullName {
// ...
public FullName copy(FullNameBuilders.Setter... setters) {
return FullNameBuilder.copy(this, setters);
}
}
Other changes
While Functional Builders are the marquee feature of this latest Jilt release, it also includes a few smaller fixes that are worth mentioning.
Propagate all annotations to setter methods
In version 1.4
, Jilt started
treating properties annotated with @Nullable
as optional automatically,
without the need to explicitly annotate them with @Opt
.
Part of that functionality was also propagating the @Nullable
annotation into the parameter of the setter method that corresponds to that property –
otherwise, IDEs and static analysis tools might report warnings for passing potentially null
values into a method that doesn’t indicate it accepts them.
As it turns out, there are other annotations beyond just @Nullable
that are useful for propagating into the setter methods –
for instance, if a property is marked as @NotNull
,
it’s helpful during static analysis if its setter parameter is annotated with that as well
(otherwise, you might accidentally pass something that allows null
values to it,
and you wouldn’t get any warnings in your build catching that potential bug).
Jilt 1.6
includes a change where all annotations
(that can be placed on parameters, of course)
are propagated to the setter method parameter corresponding to the given property,
not just @Nullable
.
Thanks to Diego Pedregal for reporting this issue, and submitting a Pull Request implementing it.
Note: there is a small bug with this feature in version 1.6
,
where some annotations, like Lombok’s @NonNull
,
are copied twice to the setter parameter.
If you get a compilation error when using Jilt 1.6
with an error message
similar to <XYZ> is not a repeatable annotation
,
make sure to update to Jilt version 1.6.1
.
Fix for toBuilder
with boolean properties
When discussing the toBuilder
functionality in the
last article about Jilt,
I described the algorithm Jilt uses for extracting the value of a given property from the target class.
However, I missed the pretty obvious fact that getters for boolean properties are typically named with the “is”
prefix instead of “get”.
Jilt version 1.6
fixes this bug, so that getters for boolean properties that start with “is”
are correctly recognized in toBuilder
methods.
Thanks to radtke for reporting this issue.
Summary
So, those are all the changes included in Jilt releases 1.6
and 1.6.1
.
I’d love any feedback you might have about these, and the library in general.
Note that Guillaume also wrote a follow-up article on his blog, discussing Jilt, and how it implements Functional Builders – I’d encourage you to give it a read as well.