📜 ⬆️ ⬇️

Extendable Classes - Extendable Builders!

Recently, I was faced with 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, providing the builder inherited from its ancestor's builder. Under the cut, I will show the course of my thoughts, bad options and the final solution of the problem.

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 approach "in the forehead", not forgetting to change the access modifier from the constructor ImmutableBase 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 order of calling the builder's methods, as sadness falls on 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 successor.

Second run


Immediately rejecting the idea of ​​redefining all the 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 extends Builder> B setSomeString(String value) { // ... return (B) this; } 

But, fortunately (there is nothing to spit on security!), This decision 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 chooses the narrowest possible type based on constraints. As we wrote extends Builder, this type is ImmutableBase.Builder. If we didn't write this, it would be Object. As a workaround, you can tell the compiler what type we are waiting for by writing this:

 MyImmutable myImmutable = (MyImmutable) builder.setSomeString("0"). <MyImmutable.Builder>setSomeInt(0).setSomeDouble(0.0).build(); 

But, you see, it will be completely inconvenient for the library user.

All your generic are belong to us!


The idea with generics was probably the right one, and therefore if you think a little more in this direction, you can get the following solution that works with our example:

 public static class Builder<B extends 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 the 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> builder = new ImmutableBase.Builder<Builder>(); MyImmutable myImmutable = builder.setSomeString("0"). setSomeInt(0).setSomeDouble(0.0).build(); } 

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 doesn’t caste anything, may cause a misunderstanding for those who have forgotten that the compiler itself inserts the necessary castes when processing generics. Fail again, and seemingly fatal: the problem is that MyImmutable.Builder is a subclass of ImmutableBase.Builder, and we cannot get rid of this conditionally.

The empire strikes back

But do not be discouraged! It is enough to show a little insight and guess that the class that sticks out to the user does not necessarily coincide with the class from which we inherit from MyImmutable. Therefore, we do this:

 public class ImmutableBase { protected ImmutableBase(InnerBuilder<?> builder) { // ... } protected static class InnerBuilder<B extends InnerBuilder<B>> { 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<Builder> {} } 


So, we have a class InnerBuilder, which is not visible to the user, and he cannot do his dirty tricks with him; and there is the Builder class, which is simply inherited from the first one, hiding implementation details from the user (ie, generics). Then it is enough in MyImmutable to declare Builder as follows:
 public static class Builder extends ImmutableBase.InnerBuilder<Builder> { public Builder setSomeDouble(double value) { // ... return this; } @Override public MyImmutable build() { return new MyImmutable(this); } } 

And enjoy life, because the problem is finally solved. Just in case, I recall two main ideas:

It is easy to understand that using this method and doing a deeper inheritance, but if you are going to do this, you probably should think about refactoring.

I hope someone will save time. Successes!

Source: https://habr.com/ru/post/125846/


All Articles