Imagine I have a class that encapsulates another class:
@Builder
public class Dragon {
private Dimensions dimensions;
private String name;
public static class ParentBuilder {
DimensionsBuilder innerBuilder = Dimensions.builder();
public DragonBuilder height(double height) {
this.innerBuilder.height(height);
return this;
}
public DragonBuilder length(double length) {
this.innerBuilder.length(length);
return this;
}
public Dragon build() {
return Dragon.builder()
.dimensions(this.innerBuilder.build())
.name(this.name)
.build();
}
}
}
@Builder
public class Dimensions {
private double height;
private double length;
}
Keep in mind that this is a very simplified example, the real code (which is, unfortunately, not about dragons) delegates a lot of properties to the innerBuilder
.
This way, I can instantiate the class like this:
Dragon dragon = Dragon.builder()
.height(12.0)
.length(25.0)
.name("Smaug")
.build();
Instead of like this:
Dragon dragon = Dragon.builder()
.dimensions(Dimensions.builder()
.height(12.0)
.length(25.0)
.build())
.name("Smaug")
.build;
Is it good coding practice to add builder methods to directly build the inner class too? Or does it offend some design principle, because maybe it's too tightly coupled?
One issue I already encountered was when doing a refactor of the inner class, I also had to apply mostly the same refactorings to the parent class.
In my opinion, there is nothing fundamentally wrong with your approach from a style/design perspective. However, as explained by user JB Nizet in the comments, there are two major problems:
@Delegate
can't help you here, because it does not work on classes generated by Lombok itself.)dimensions(Dimensions)
and the delegation methods, which is very confusing.From a user perspective, I would like the builder to be used like this:
Dragon dragon = Dragon.builder()
.dimensions()
.height(12.0)
.length(25.0)
.back()
.name("Smaug")
.build();
This is how you can achieve it (using Lombok 1.18.8):
@Builder
public class Dragon {
private Dimensions dimensions;
private String name;
public static class DragonBuilder {
private Dimensions.DimensionsBuilder innerBuilder =
new Dimensions.DimensionsBuilder(this);
// If a method of the same name exists, Lombok does not generate
// another one even if the parameters differ.
// In this way, users cannot set their own dimensions object.
public Dimensions.DimensionsBuilder dimensions() {
return innerBuilder;
}
// Customize build() so that your innerBuilder is used to create
// the Dimensions instance.
public Dragon build() {
return new Dragon(innerBuilder.build(), name);
}
}
}
The builder for Dimensions
holds a reference to the container DragonBuilder
:
// Don't let Lombok create a builder() method, so users cannot
// instantiate builders on their own.
@Builder(builderMethodName = "")
public class Dimensions {
private double height;
private double length;
public static class DimensionsBuilder {
private Dragon.DragonBuilder parentBuilder;
// The only constructor takes a reference to the containing builder.
DimensionsBuilder(Dragon.DragonBuilder parentBuilder) {
this.parentBuilder = parentBuilder;
}
// Provide a method that returns the containing builder.
public Dragon.DragonBuilder back() {
return parentBuilder;
}
// The build() method should not be called directly, so
// we make it package-private.
Dimensions build() {
return new Dimensions(height, length);
}
}
}
This approach scales because Lombok automatically generates all necessary remaining setter methods in the builders.
Furthermore, there may be no surprises due to users providing their own Dimensions
instance. (You could allow that, but I strongly suggest to do runtime checks for potential conflicts then, e.g. by checking whether both methods have been called.)
The drawback is that Dimensions.builder()
is not available any more, so it cannot be used directly or in builders of other classes that have a Dimensions
field. However, there is also a solution for that: Use @SuperBuilder Dimensions
and define a class NestedDimensionsBuilder extends Dimensions.DimensionsBuilder<Dimensions, NestedDimensionsBuilder>
within DragonBuilder
.