I am refactoring some code in to a builder pattern, and running in to an issue when sub-classing a builder for a child class.
When I have a builder subclass, and I try to chain a method in the parent, it is returning the parent builder, so my instance no longer has access to the child class methods.
public class App {
public static void main(String[] args) {
Parent p;
p = new App().new ChildBuilder()
.withName("Test")
.withNickName("Test1")
.build();// Doesn't Compile
p = new App().new ChildBuilder()
.withNickName("Test1")
.withName("Test")
.build();
}
class Parent {
public Parent(ParentBuilder builder) {}
}
class Child extends Parent {
public Child(ChildBuilder builder) { super(builder); }
}
class ParentBuilder {
private String name;
public ParentBuilder() {}
public Parent build() { return new Parent(this); }
public ParentBuilder withName(String name) {
this.name = name;
return this;
}
}
class ChildBuilder extends ParentBuilder {
private String nickName;
public ChildBuilder withNickName(String nickName) {
this.nickName = nickName;
return this;
}
}
}
The second line in the main method won't compile because withName("Test")
is on the ParentBuilder class and returns a ParentBuilder. Reordering the chains to call all the ChildBuilder methods first fixes the problem, but that sounds like a terrible experience for the people using my api (myself included).
If I add an override in the child I can make it work through covariance:
@Override
public ChildBuilder withName(String name) {
super.withName(name);
return this;
}
But this is a lot of boilerplate code that I would rather not maintain (each parent builder could have a handful of subclasses, so I would need to rewrite these methods in each subclass for each method in the parent class).
Is there a way to do what I am trying to do without the overrides? Can java "infer" the covariant methods in the children?
I am also worried that this issue is indicative that I am designing the builders incorrectly.
No, there is no inferred co variance, but it is mimicked by the curiously recurring template pattern (or CRTP, originated in C++).
You can solve this by adding 2 (package private) abstract classes that use CRTP (i.e., the parameter type is the subclass). The builder functionality is moved to those classes, and then you create 2 empty classes that extend the abstract builders.
I've also changed the constructors to not depend on the builder directly, but on the actual arguments, since that is usually how it's done, and it makes the implementation a little cleaner here:
public static void main(String[] args) {
// Both examples now compile
Parent p;
p = new App().new ChildBuilder()
.withName("Test")
.withNickName("Test1")
.build();
p = new App().new ChildBuilder()
.withNickName("Test1")
.withName("Test")
.build();
}
class Parent {
public Parent(String name) {}
}
class Child extends Parent {
public Child(String name, String nickName) { super(name); }
}
abstract class AbstractParentBuilder<T extends AbstractParentBuilder<T>> {
protected String name;
protected AbstractParentBuilder() {}
public Parent build() { return new Parent(name); }
@SuppressWarnings("unchecked")
public T withName(String name) {
this.name = name;
return (T) this;
}
}
class ParentBuilder extends AbstractParentBuilder<ParentBuilder> {}
abstract class AbstractChildBuilder<T extends AbstractChildBuilder<T>> extends AbstractParentBuilder<T> {
protected String nickName;
protected AbstractChildBuilder() {}
public Child build() { return new Child(name, nickName); }
@SuppressWarnings("unchecked")
public T withNickName(String nickName) {
this.nickName = nickName;
return (T) this;
}
}
class ChildBuilder extends AbstractChildBuilder<ChildBuilder> {}