The HTML version of this document is generated using Asciidoctor.
The following extensions are used:
The command is: asciidoctor -r asciidoctor-diagram *.adoc.
This is a Java representation of the OpenAPI Specification.
The model doesn’t contain any serialization annotation.
Serialization / Deserialization is the job of the Parser.
link:Model.mmd[role=include]
link:Model.puml[role=include]
The Specification Extensions are not implemented.
Should we use builders ?
Consider a builder when faced with many constructor parameters
Effective Java Third Edition
Not that the typical immutability perk that comes with the builder pattern is of no need here since records are immutable on their own.
public record Info(@NotNull String title, String summary, String description, URL termsOfService, Contact contact, License license, @NotNull String version) { } (1)-
You don’t get more concise than that!
// All fields
var contact = new Contact("name", new URL(...), "email");
var license = new License("name", "identifier");
var info = new Info("title", "summary", "description", new URL("..."), contact, license, "version"); (1)
// Required fields only
var info = new Info("title", null, null, null, null, null, "version"); (2)
// One optional field
var info = new Info("title", null, "description", null, null, null, "version"); (2)-
You don’t know what each value means
-
You have to pass
nullfor each optional parameter
public record Info(@NotNull String title, String summary, String description, URL termsOfService, Contact contact, License license, @NotNull String version) {
public static class Builder { (1)
private final String title;
private String summary;
private String description;
private URL termsOfService;
private Contact contact;
private License license;
private final String version;
public Builder(String title, String version) {
this.title = title;
this.version = version;
}
public Builder summary(String summary) {
this.summary = summary;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder termsOfService(URL termsOfService) {
this.termsOfService = termsOfService;
return this;
}
public Builder contact(Contact contact) {
this.contact = contact;
return this;
}
public Builder license(License license) {
this.license = license;
return this;
}
public Info build() {
return new Info(this);
}
}
private Info(Builder builder) {
this(builder.title, builder.summary, builder.description, builder.termsOfService, builder.contact, builder.license, builder.version);
}
}-
The readability is lost.
Most of this code is just noise!
// All fields
var contact = new Contact("name", new URL(...), "email");
var license = new License("name", "identifier");
var info = new Info.Builder("title", "version") (1)
.summary("summary") (2)
.description("description") (2)
.termsOfService(new URL("...")) (2)
.contact(contact) (2)
.license(license) (2)
.build();
// Required fields only
var info = new Info.Builder("title", "version").build(); (3) (4)
// One optional field
var info = new Info.Builder("title", "version")
.description("description") (2)
.build(); (3)-
The mandatory parameters must be passed to the builder constructor
-
The meaning of each value is very clear
-
That’s pretty concise!
-
This can be achieved by adding an
Infoconstructor with just 2 parameters.
public record Info(@NotNull String title, String summary, String description, URL termsOfService, Contact contact, License license, @NotNull String version) { } (1)-
The record stays unchanged!
package openapi.model.v310.builder;
public class InfoBuilder { (1)
private final String title;
private String summary;
private String description;
private URL termsOfService;
private Contact contact;
private License license;
private final String version;
public InfoBuilder(String title, String version) {
this.title = title;
this.version = version;
}
public InfoBuilder summary(String summary) {
this.summary = summary;
return this;
}
public InfoBuilder description(String description) {
this.description = description;
return this;
}
public InfoBuilder termsOfService(URL termsOfService) {
this.termsOfService = termsOfService;
return this;
}
public InfoBuilder contact(Contact contact) {
this.contact = contact;
return this;
}
public InfoBuilder license(License license) {
this.license = license;
return this;
}
public Info build() {
return new Info(this.title, this.summary, this.description, this.termsOfService, this.contact, this.license, this.version);
}
}-
The builder is now a standalone class.
It is still a lot of noise but at least it is not part of the record.
|
Note
|
Builder Design
The following traditional builder design decisions don’t apply anymore : Static inner class
Traditional builders are inner classes because the builder is only useful to the outer class [1]. Private constructor
If the builder is an inner class, the outer class constructor can be In our case, we want to allow the clients to use both the builder and the constructor [2]. Constructor argument
If only the builder can be used to instantiate the class, then the constructor can take a builder instance as its sole argument. |
// All fields
var contact = new Contact("name", new URL(...), "email");
var license = new License("name", "identifier");
var info = new InfoBuilder("title", "version") (1)
.summary("summary")
.description("description")
.termsOfService(new URL("..."))
.contact(contact)
.license(license)
.build();
// Required fields only
var info = new InfoBuilder("title", "version").build(); (1)
// One optional field
var info = new InfoBuilder("title", "version") (1)
.description("description")
.build();-
The only difference between this code and the previous one is that we use
new InfoBuilderinstead ofnew Info.Buildersince the builder is now outside the record.
If some project out there wants to have code generators for patterns that are sometimes useful for records, that’s great — but that’s not where the language should be focusing.
Re: Builder pattern for Java records
Using Lombok
pom.xml<project>
<properties>
<lombok.version>1.18.20</lombok.version>
</properties>
<dependencies>
<dependency> <!--(1)-->
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>model</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath> <!--(2)-->
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>-
Add the
Lombokdependency -
Configure the compiler plugin to use it
As mentioned in this post, it is possible to annotate the constructor and generate the builder automatically.
public record Info(@NotNull String title, String summary, String description, URL termsOfService, Contact contact, License license, @NotNull String version) {
@Builder(builderMethodName = "hiddenBuilder") (1)
public Info {} (2)
public static InfoBuilder builder(String title, String version) { (3)
return hiddenBuilder().title(title).version(version);
}
}-
The
Builderannotation with the hidden builder will generate a builder very similar to the hand-written builder. The client code will therefore be very similar to the builder’s client code [3].
The problem is that the model is now annotated with a non-standard Java annotation. -
For this to work, you have to create a compact constructor.
-
For this to work, you have to add this
builderfactory method [4].
Using RecordBuilder
<project>
<properties>
<builder.version>1.19</builder.version>
</properties>
<dependencies>
<dependency> <!--(1)-->
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<version>${builder.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>model</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath> <!--(2)-->
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${builder.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors> <!--(2)-->
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
</project>-
Add the
RecordBuilderdependency -
Configure the compiler plugin to use it.
Themvn compilecommand will generate the classes in thetarget/generated-sources/annotationsfolder.
As mentioned here, this library allows us to generate the builder without annotating the model.
@RecordBuilder.Include({Info.class})
public class OpenApiBuilder { (1)
}-
Even though the model is not annotated, this class has to be added.
This doesn’t generate a builder with the mandatory parameters.
-
Should we use a custom implementation that returns an
Optionalor should we passOptionalto the record or should we leave it this way ? -
What should be done with Specification Extensions ?
-
Should we continue using the Bean Validation API ?
static so that it can be instantiated without creating an instance of the outer class
Info.builder(…) instead of new Info.Builder(…)
Info constructor with just 2 parameters