问题
I have a certain class structure that looks something like this:
class Parent {
public Parent(int property) { /* use property */}
}
class Son extends Parent {
public Son(int parentProperty, String sonProperty) {
super(parentProperty);
/* use son property */
}
}
I'd like to create builders for both these classes such that:
class ParentBuilder {
protected int parentProperty;
public ParentBuilder parentProperty(int parentPropertyValue) {
parentPropertyValue = parentPropertyValue;
return this;
}
public Parent build() {
return new Parent(parentProperty);
}
}
class SonBuilder extends ParentBuilder {
private String sonProperty;
public SonBuilder sonProperty(String sonProperty) {
this.sonProperty = sonProperty;
return this;
}
@Override
public Son build() {
return new Son(parentProperty, sonProperty);
}
}
but this causes the following problem:
SonBuilder sonBuilder = new SonBuilder();
sonBuilder.sonProperty("aString").build(); // this works and creates Son
sonBuilder.sonProperty("aString").parentProperty(1).build(); // this works and creates Parent instead of Son
sonBuilder.parentProperty(1).sonProperty("aString").build(); // this doesn't work
I realize I'm nitpicking and this could be solved with just not returning this
(i.e. without method chaining), but I'm wondering if there is an elegant solution.
edit
It seems that the word "elegant" is a source for a bit of confusion.
By "elegant" I mean a solution which allows for method chaining and does not involve casting.
回答1:
First point
sonBuilder.sonProperty("aString").parentProperty(1).build();
this works and creates Parent instead of Son
It is expected as parentProperty()
returns a ParentBuilder
:
public ParentBuilder parentProperty(int parentPropertyValue) {...
And ParentBuilder.build()
creates a Parent
:
public Parent build() {
return new Parent(parentProperty);
}
Second point
sonBuilder.parentProperty(1).sonProperty("aString").build(); // this doesn't work
As said in the first point, parentProperty()
returns a ParentBuilder
.
And ParentBuilder
of course doesn't have a sonProperty()
method.
So it cannot compile.
I'm wondering if there is an elegant solution.
An elegant solution would be not make SonBuilder
inherited ParentBuilder
but compose with a ParentBuilder
field.
For example :
class SonBuilder {
private String sonProperty;
private ParentBuilder parentBuilder = new ParentBuilder();
public SonBuilder sonProperty(String sonProperty) {
this.sonProperty = sonProperty;
return this;
}
public SonBuilder parentProperty(int parentPropertyValue) {
parentBuilder.parentProperty(parentPropertyValue);
return this;
}
public Son build() {
return new Son(parentBuilder.parentProperty, sonProperty);
}
}
You can so create the Son
in this way :
SonBuilder sonBuilder = new SonBuilder();
Son son = sonBuilder.sonProperty("aString").parentProperty(1).build();
回答2:
I'm not sure if it can be considered as elegant, but you can use casting:
SonBuilder sonBuilder = new SonBuilder();
Son son1 = sonBuilder.sonProperty("aString").build();
Son son2 = (Son) sonBuilder.sonProperty("aString").parentProperty(1).build();
Son son3 = ((SonBuilder) sonBuilder.parentProperty(1)).sonProperty("aString").build();
回答3:
You can test for the parent property to exist in your build()
method.
return (parentProperty == null) ? new Parent(parentProperty, sonProperty) : new Son(sonProperty);
回答4:
Override parentProperty
in SonBuilder
:
@Override
public SonBuilder parentProperty(int parentPropertyValue) {
super.parentProperty(parentPropertyValue);
return this;
}
This is safe because (and as long as) super.parentProperty
returns this
. If you have immutable builders instead, your override would also be different.
Or use generics:
abstract class AParentBuilder<T extends Parent> {
protected int parentProperty;
public AParentBuilder<T> parentProperty(int parentPropertyValue) {
parentPropertyValue = parentPropertyValue;
return this;
}
abstract public T build();
}
class ParentBuilder extends AParentBuilder<Parent> {
@Override
public Parent build() {
return new Parent(parentProperty);
}
}
abstract class ASonBuilder<T extends Son> extends AParentBuilder<T> {
private String sonProperty;
public ASonBuilder<T> sonProperty(String sonProperty) {
this.sonProperty = sonProperty;
return this;
}
}
class SonBuilder extends ASonBuilder<Son> {
@Override
public Son build() {
return new Son(parentProperty, sonProperty);
}
}
回答5:
An alternative way is using covariant return type in the subclass method.
Keep SonBuilder
extend ParentBuilder
but override parentProperty()
to return a SonBuilder
:
@Override
public SonBuilder parentProperty(int parentPropertyValue) {
super.parentProperty(parentPropertyValue);
return this;
}
You can still create the Son in this way :
SonBuilder sonBuilder = new SonBuilder();
Son son = sonBuilder.sonProperty("aString").parentProperty(1).build();
as now parentProperty(1)
invokes the subclass method that returns a SonBuilder
.
This solution may seem fine but it has a drawback.
It creates a strong coupling between the subclass builder and the base builder.
At each time you add a setProperty method in the parent builder, you have to override it in the subclass.
Otherwise, you would still have the initial problem : a code that doesn't compile or it would instantiate the Parent instead of the Son class.
For classes that don't change at all, it may be acceptable.
Otherwise, it should be avoided.
The solution using the composition is better as it allows the SonBuilder
to evolve at its own rhythm and with its own rules.
来源:https://stackoverflow.com/questions/47197777/returning-inherited-class-instead-of-superclass-in-method-overriding