mr.nothing
mr.nothing

Reputation: 5399

Java fluent builder and inheritance

One more day and one more struggle with generics.

I have set of Control objects with the following inheritance tree:

BaseControl
|_SimpleControl
  |_MultipleControl
    |_AutocompleteControl
    |_SelectControl

For each of non-abstract object in this tree I'd like to provide builder so these objects can be created easily. Here is what I have so far:

BaseControlBuilder:

public abstract class BaseControlBuilder<C extends BaseControl, B extends BaseControlBuilder<C, B>> {
    protected C control;
    private B builder;

    BaseControlBuilder() {
        control = createObj();
        builder = getThis();
    }
    public C build() { return control; }

    protected abstract C createObj();
    protected abstract B getThis();
}

SimpleControlBuilder:

public class SimpleControlBuilder<C extends SimpleControl, B extends SimpleControlBuilder<C, B>>
        extends BaseControlBuilder<SimpleControl, SimpleControlBuilder<C, B>> {

    public SimpleControlBuilder(final String id, final String caption,
            final InputType controlType) {
        super();
        control.setId(id);
        control.setCaption(caption);
        control.setType(controlType);
    }

    public SimpleControlBuilder(final InputType controlType) {
        this("", "", controlType);
    }

    public SimpleControlBuilder(final Enum<?> en, final InputType controlType) {
        this(en.name(), en.toString(), controlType);
    }

    public SimpleControlBuilder<C, B> disabled() {
        control.setDisabled(true);
        return this;
    }

    @Override
    protected SimpleControl createObj() {
        return new SimpleControl();
    }

    @Override
    protected SimpleControlBuilder<C, B> getThis() {
        return this;
    }
}

MultipleControlBuilder:

abstract class MultipleControlBuilder<C extends MultipleControl, B extends MultipleControlBuilder<C, B>>
        extends SimpleControlBuilder<MultipleControl, MultipleControlBuilder<C, B>> {

    MultipleControlBuilder(final InputType type) {
        super(type);
    }

    MultipleControlBuilder(final String id, final String caption,
            final InputType type) {
        super(id, caption, type);
    }

    MultipleControlBuilder(final Enum<?> en, final InputType type) {
        super(en, type);
    }

    public MultipleControlBuilder<C, B> multiple() {
        ((MultipleControl) control).setMultiple(true);
        return this;
    }
}

AutocompleteControlBuilder:

public class AutocompleteControlBuilder<C extends AutocompleteControl, B extends AutocompleteControlBuilder<C, B>>
    extends MultipleControlBuilder<AutocompleteControl, AutocompleteControlBuilder<C, B>> {

    public AutocompleteControlBuilder(final String url,
            final AutocompleteType autocompleteType) {
        this("", "", url, autocompleteType);
    }

    public AutocompleteControlBuilder(final String id,
            final String caption, final String url,
            final AutocompleteType autocompleteType) {
        super(id, caption, InputType.AUTOCOMPLETE);
        ((AutocompleteControl) control).setAutocompleteUrl(url);
        ((AutocompleteControl) control).setAutocompleteType(autocompleteType);
    }

    public AutocompleteControlBuilder(final Enum<?> en, final String url,
            final AutocompleteType autocompleteType) {
        this(en.name(), en.toString(), url, autocompleteType);
    }

    @Override
    protected AutocompleteControl createObj() {
        return new AutocompleteControl();
    }

    @Override
    protected AutocompleteControlBuilder<C, B> getThis() {
        return this;
    }
}

But surprisingly I have got some unexpected results.
For instance, in the following code I have to cast control to MultipleControl to call setter despite the fact that C extends MultipleControl...

Moreover, the following build() method call: new AutocompleteControlBuilder<AutocompleteControl, AutocompleteControlBuilder>("url", AutocompleteType.STANDARD).build()); returns SimpleControl instead of AutocompleteControl which doesn't make sense, cause I explicitly provided type parameter.

And the last straw is that conciseness and clear code I'm trying to achieve are killed by ugly new AutocompleteControlBuilder<AutocompleteControl, AutocompleteControlBuilder> constructor call. Could anybody point me to the best practices of solving this problem?

Upvotes: 2

Views: 332

Answers (1)

EpicPandaForce
EpicPandaForce

Reputation: 81568

Okay, in order to set this up correctly, you should make some changes:

public class SimpleControlBuilder<C extends SimpleControl, B extends SimpleControlBuilder<C, B>>
        extends BaseControlBuilder<SimpleControl, SimpleControlBuilder<C, B>> { // this should extend with the extension classes

    public SimpleControlBuilder(final String id, final String caption,
            final InputType controlType) {
        super();
        control.setId(id);
        control.setCaption(caption);
        control.setType(controlType);
    }

    public SimpleControlBuilder(final InputType controlType) {
        this("", "", controlType);
    }

    public SimpleControlBuilder(final Enum<?> en, final InputType controlType) {
        this(en.name(), en.toString(), controlType);
    }

    public SimpleControlBuilder<C, B> disabled() { // this should return B
        control.setDisabled(true);
        return this;
    }

    @Override
    protected SimpleControl createObj() { // this should return C
        return new SimpleControl();
    }

    @Override
    protected SimpleControlBuilder<C, B> getThis() { // this should return B
        return this;
    }
}

So that means

public abstract class SimpleControlBuilder<C extends SimpleControl, B extends SimpleControlBuilder<C, B>>
        extends BaseControlBuilder<C, B> {

    public SimpleControlBuilder(final String id, final String caption,
            final InputType controlType) {
        super();
        control.setId(id);
        control.setCaption(caption);
        control.setType(controlType);
    }

    public SimpleControlBuilder(final InputType controlType) {
        this("", "", controlType);
    }

    public SimpleControlBuilder(final Enum<?> en, final InputType controlType) {
        this(en.name(), en.toString(), controlType);
    }

    public B disabled() {
        control.setDisabled(true);
        return getThis();
    }
}

And

abstract class MultipleControlBuilder<C extends MultipleControl, B extends MultipleControlBuilder<C, B>>
        extends SimpleControlBuilder<C, B> {

    MultipleControlBuilder(final InputType type) {
        super(type);
    }

    MultipleControlBuilder(final String id, final String caption,
            final InputType type) {
        super(id, caption, type);
    }

    MultipleControlBuilder(final Enum<?> en, final InputType type) {
        super(en, type);
    }

    public B multiple() {
        control.setMultiple(true);
        return getThis();
    }
}

And

public abstract class AutocompleteControlBuilder<C extends AutocompleteControl, B extends AutocompleteControlBuilder<C, B>>
    extends MultipleControlBuilder<C, B>> {

    public AutocompleteControlBuilder(final String url,
            final AutocompleteType autocompleteType) {
        this("", "", url, autocompleteType);
    }

    public AutocompleteControlBuilder(final String id,
            final String caption, final String url,
            final AutocompleteType autocompleteType) {
        super(id, caption, InputType.AUTOCOMPLETE);
        control.setAutocompleteUrl(url);
        control.setAutocompleteType(autocompleteType);
    }

    public AutocompleteControlBuilder(final Enum<?> en, final String url,
            final AutocompleteType autocompleteType) {
        this(en.name(), en.toString(), url, autocompleteType);
    }
}

This works if MultipleControl extends SimpleControl and AutocompleteControl extends MultipleControl, and you have concrete extensions of SimpleControl that can return themselves with getThis() with concretized parameters.

public class SomeControlBuilder extends MultipleControlBuilder<SomeControl, SomeControlBuilder> {
    public SomeControlBuilder(final InputType type) {
        super(type);
    }

    public SomeControlBuilder(final String id, final String caption,
            final InputType type) {
        super(id, caption, type);
    }

    public SomeControlBuilder(final Enum<?> en, final InputType type) {
        super(en, type);
    }

    @Override
    protected SomeControlBuilder getThis() {
        return this;
    }

    @Override
    protected SomeControl createObj() {
        return new SomeControl();
    }
}

Upvotes: 1

Related Questions