Extensible Classes - Extensible Builders!
- Tutorial
Recently, I came across a task that turned out to be much less trivial than it seemed at first glance. Let there be some class (in my example immutable) for which there is a builder. You must be able to inherit from this class by providing a builder inherited from the builder of its ancestor. Under the cut, I will show the course of my thoughts, unsuccessful options and the final solution to the problem.
Now we need to try to create some class that inherits from ImmutableBase. Let’s try the “forehead” approach, not forgetting to change the access modifier of the ImmutableBase constructor to protected:
At first glance, it seems that everything is fine. However, you only need to swap the call of the methods of the builder, how sadness falls upon us:
Indeed, the methods defined in ImmutableBase.Builder return an instance of it, not an instance of its descendant.
Immediately dismissing the idea of overriding all methods from his ancestor in MyImmutable.Builder, we begin to think. A tempting option seems to spit on security and do something like
But, fortunately (there is nothing to spit on safety!), This solution will not work, giving all the same error. The fact is that in this case, java cannot itself infer a type based on how it is used, and therefore selects the narrowest possible type based on restrictions. Since we wrote extends Builder, this type is ImmutableBase.Builder. If we did not write this, it would be Object. You can tell the compiler as a workaround what type we are waiting for there by writing this:
But, you must admit, it will not be convenient for the library user.
The idea with generic was probably correct, and therefore, if you think a little more in this direction, you can get the following solution working on our example:
However, even here terminal happiness is not reached: we got something not type-safe, and getting the error again is quite simple:
at startup will cause
That, given the fact that the user did not cast anything, may cause confusion for those who have forgotten that the compiler, when processing generics, inserts the necessary castes itself. The file is again, and it would seem fatal: the problem is that MyImmutable.Builder is a subclass of ImmutableBase.Builder, and we cannot get rid of this by condition.
So, we have the InnerBuilder class, which the user cannot see, and he cannot do his dirty tricks with him; and there is a Builder class that simply inherits from the first, hiding implementation details (i.e. generics) from the user. Then it is enough to declare Builder in MyImmutable as follows:
And enjoy life, because the task is finally solved. Just in case, let me remind you of two main ideas:
It is easy to understand that using this method it is easy to do a deeper inheritance, but if you are planning to do this, you most likely should think about refactoring.
Hope this saves someone time. Good luck!
Let's start with the obvious
We formalize the conditions of our problem in the code:public class ImmutableBase {
private ImmutableBase(Builder builder) {
// ...
}
public static class Builder {
public Builder setSomeString(String value) {
// ...
return this;
}
public Builder setSomeInt(int value) {
// ...
return this;
}
public ImmutableBase build() {
return new ImmutableBase(this);
}
}
}
Now we need to try to create some class that inherits from ImmutableBase. Let’s try the “forehead” approach, not forgetting to change the access modifier of the ImmutableBase constructor to protected:
public class MyImmutable extends ImmutableBase {
protected MyImmutable(Builder builder) {
super(builder);
// do more things
}
public static class Builder extends ImmutableBase.Builder {
public Builder setSomeDouble(double value) {
// ...
return this;
}
@Override
public MyImmutable build() {
return new MyImmutable(this);
}
}
public static void main(String[] args) {
Builder builder = new Builder();
MyImmutable myImmutable = builder.setSomeDouble(0.0).
setSomeInt(0).setSomeString("0").build();
}
}
At first glance, it seems that everything is fine. However, you only need to swap the call of the methods of the builder, how sadness falls upon us:
MyImmutable.java:20: cannot find symbol
symbol : method setSomeDouble(double)
location: class ImmutableBase.Builder
setSomeInt(0).setSomeDouble(0.0).build();
^
Indeed, the methods defined in ImmutableBase.Builder return an instance of it, not an instance of its descendant.
Second run
Immediately dismissing the idea of overriding all methods from his ancestor in MyImmutable.Builder, we begin to think. A tempting option seems to spit on security and do something like
public B setSomeString(String value) {
// ...
return (B) this;
}
But, fortunately (there is nothing to spit on safety!), This solution will not work, giving all the same error. The fact is that in this case, java cannot itself infer a type based on how it is used, and therefore selects the narrowest possible type based on restrictions. Since we wrote extends Builder, this type is ImmutableBase.Builder. If we did not write this, it would be Object. You can tell the compiler as a workaround what type we are waiting for there by writing this:
MyImmutable myImmutable = (MyImmutable) builder.setSomeString("0").
setSomeInt(0).setSomeDouble(0.0).build();
But, you must admit, it will not be convenient for the library user.
All your generic are belong to us!
The idea with generic was probably correct, and therefore, if you think a little more in this direction, you can get the following solution working on our example:
public static class Builder> {
public B setSomeString(String value) {
// ...
return (B) this;
}
public B setSomeInt(int value) {
// ...
return (B) this;
}
public ImmutableBase build() {
return new ImmutableBase(this);
}
}
However, even here terminal happiness is not reached: we got something not type-safe, and getting the error again is quite simple:
public static void main(String[] args) {
ImmutableBase.Builder builder = new ImmutableBase.Builder();
MyImmutable myImmutable = builder.setSomeString("0").
setSomeInt(0).setSomeDouble(0.0).build();
}
at startup will cause
Exception in thread "main" java.lang.ClassCastException: ImmutableBase$Builder cannot be cast to MyImmutable$Builder
at MyImmutable.main(MyImmutable.java:24)
That, given the fact that the user did not cast anything, may cause confusion for those who have forgotten that the compiler, when processing generics, inserts the necessary castes itself. The file is again, and it would seem fatal: the problem is that MyImmutable.Builder is a subclass of ImmutableBase.Builder, and we cannot get rid of this by condition.
The empire strikes back
However, do not lose heart! It’s enough to show a little insight and guess that the class that sticks out to the user does not have to coincide with the class from which we inherit from MyImmutable. Therefore, we will do this:public class ImmutableBase {
protected ImmutableBase(InnerBuilder builder) {
// ...
}
protected static class InnerBuilder> {
public B setSomeString(String value) {
// ...
return(B) this;
}
public B setSomeInt(int value) {
// ...
return (B)this;
}
public ImmutableBase build() {
return new ImmutableBase(this);
}
}
public static class Builder extends InnerBuilder {}
}
So, we have the InnerBuilder class, which the user cannot see, and he cannot do his dirty tricks with him; and there is a Builder class that simply inherits from the first, hiding implementation details (i.e. generics) from the user. Then it is enough to declare Builder in MyImmutable as follows:
public static class Builder extends ImmutableBase.InnerBuilder {
public Builder setSomeDouble(double value) {
// ...
return this;
}
@Override
public MyImmutable build() {
return new MyImmutable(this);
}
}
And enjoy life, because the task is finally solved. Just in case, let me remind you of two main ideas:
- Make Builder a generic class so that the return type matches expected
- Separate what will be inherited from and what the user will see, so that he can not maliciously use these generics and force them to cast to something wrong.
It is easy to understand that using this method it is easy to do a deeper inheritance, but if you are planning to do this, you most likely should think about refactoring.
Hope this saves someone time. Good luck!