The correct polymorphic builder in Java
What is all this about?
When implementing chained builder in Java, everything is fine until you need to add inheritance. Two problems immediately arise - how to make the methods of the parent builder return the object of the child builder and how to pass the child builder to functions that accept the parent. An implementation of the pattern is proposed that allows solving both problems. Sources can be viewed here on the github.
Upd. Real problem
The application has dto-objects for displaying the result, which are constructed as follows:
1) A builder of the desired dto-object is created.
2) The builder is passed to various classes in a chain, each class uses a builder to set the fields it needs.
One fine day, they decided to introduce a new version of the API, the dto-object was expanded using inheritance, and it turned out that its builder could not be inserted into the existing chain of classes for completion.
Formulation of the problem
There should be 100500 words about the importance of the pattern builder, so as not to bore the reader with this crap, let's get down to business immediately. Let there be 3 classes with the friendly names Gen1, Gen2 and Gen3. They form a linear hierarchy of Gen3 → Gen2 → Gen1. Each of them contains exactly one very important method called setValX (where X is the digit from the class name). We want to get builders Builder1, Builder2, Builder3 each of which contains the corresponding valX method, which is implemented only in one class (we do not want to copy-paste).
The chains should also work:
Gen1 gen1 = builder1.val1("val1").build();
Gen2 gen2 = builder2.val1("val1").val2("val2").build();
Gen3 gen3 = builder3.val1("val1").val2("val2").val3("val3").build();
And the ability to use child builders instead of parent ones:
Gen1 someFunction(Builder1 builder1) {
return builder1.val1("val1111");
}
...
someFunction1(builder3.val2("val222").val3("val333"));
What and how it happened
The builder is supposed to be made according to the following scheme - create an object at the very beginning, then fill in its fields, and return it to the client in the build () function. In this case, we need a class that will do a simple thing - to store a link to an object to be built and store a link to the builder of the desired type, which all setter methods will return. The following class solves the problem:
public class BuilderImpl {
protected T nested;
RetBuilder returnBuilder;
protected BuilderImpl(T child) {
nested = child;
}
protected T getNested() {
return nested;
}
protected void injectReturnBuilder(RetBuilder builder) {
returnBuilder = builder;
}
protected RetBuilder self() {
return returnBuilder;
}
public T build() {
return nested;
}
}
Of course, it would be better to get rid of the injectReturnBuilder method by passing the necessary data to the constructor, but alas, this will be passed there to the child builder, which cannot be used until the parent constructor super () ends. The getNested () method is an amateur, you can access the nested field directly. The self () method is made not to confuse the field with the word this.
Now let's think about this problem. If we have some generic Builder1 <> that implements everything that we need for the Gen1 class (with some parameters Gen1, Builder1), we will need to inherit the generic Builder2 for Gen2 from it (with some parameters Gen1, Builder1), and from that Builder3 for Gen3 it turns out that Builder3 in the ancestors has two implementations of the original Builder1 with different parameters, which, alas, is strictly prohibited by Java.
But there is a way out - it is necessary to separate the configuration of the object fields and the creation of the object into different classes.
Classes named InnerBuilderX are responsible for setting fields and returning an object and allow inheritance. Classes with the names FinalBuilderX are inherited from the corresponding InnerBuilderX, adding the creation of the source object and are not allowed to further inheritance.
Writing InnerBuilderX with the right combination of wildcard presents a separate difficulty. Through long trial and error (reading the specifications is not our way) was found an acceptable option. But while he was found, combinations were tried, for which the Idea inspector died or made mistakes, which somewhat slowed down the development. And so, here is the code for the InnerBuilder1 class of Gen1. Parameter T is the type of the stored object, RetBuilder is the type of builder that is returned from the installation function val1.
public static class InnerBuilder1>
extends BuilderImpl {
protected InnerBuilder1(T created) {
super(created);
}
public RetBuilder val1(String val) {
getNested().setVal1(val);
return self();
}
}
Of course, the recursive construction of class InnerBuilder1
Well, FinalBuilder is pretty simple:
private static class FinalBuilder1 extends InnerBuilder1 {
private FinalBuilder1() {
super(new Gen1()); // сюда нельзя this
injectReturnBuilder(this);
}
}
It remains to add a static function to create a builder:
public static InnerBuilder1 builder() {
return new FinalBuilder1();
}
Now let's move on to the child builder. We inherit the implementation for the internal builder and make the creation of the object in the final:
public static InnerBuilder2 builder() {
return new FinalBuilder2();
}
public static class InnerBuilder2> extends InnerBuilder1 {
protected InnerBuilder2(T created) {
super(created);
}
public RetBuilder val2(String val) {
getNested().setVal2(val);
return self();
}
}
private static class FinalBuilder2 extends InnerBuilder2 {
private FinalBuilder2() {
super(new Gen2());
injectReturnBuilder(this);
}
}
You can try compiling the test code:
Gen2.builder().val1("111").val1("111").val1("111").val1("111").val2("222").build();
Happened! And what about polymorphism?
// принимает билдер передкового Gen1
Gen1 buildGen1Final(Gen1.InnerBuilder1 builder) {
builder.val1("set value from Gen1 builder");
return builder.build();
}
...
// а получает билдер потомка Gen2
buildGen1Final(
Gen2.builder().val2("set value from Gen2 builder")
);
Everything works too. Similarly, a builder for the Gen3 class is implemented, for details you can contact the github