user5182503
user5182503

Reputation:

Builder pattern with inheritance support generics issue

I implemented pattern based on this answer I have the following asbtract config:

public abstract class AbstractConfig {

    public static abstract class Builder<B extends Builder<B>> {

        private int calories = 0;

        public Builder() {

        }

        public B setCalories(int calories) {
            this.calories = calories;
            return (B) this;
        }

        public abstract AbstractConfig build();
    }

    private int calories = 0;

    protected AbstractConfig(final Builder builder) {
        calories = builder.calories;
    }
}

And I have the following concrete config:

public class DialogConfig extends AbstractConfig {

    public static class DialogConfigBuilder<B extends DialogConfigBuilder<B>> extends Builder<B> {

        private double width;

        private double height;

        public DialogConfigBuilder() {
            //does nothing.
        }

        public B setWidth(final double value) {
            width = value;
            return (B) this;
        }

        public B setHeight(final double value) {
            height = value;
            return (B) this;
        }
        public DialogConfig build() {
            return new DialogConfig(this);
        }
    }

    private final double width;

    private final double height;

    protected DialogConfig(final DialogConfigBuilder builder) {
        super(builder);
        width = builder.width;
        height = builder.height;
    }

    public double getWidth() {
        return width;
    }

    public double getHeight() {
        return height;
    }
}

And this is how I use it

DialogConfig config = new DialogConfig.DialogConfigBuilder()
                .setWidth(0)
                .setCalories(0)
                .setHeight(0) //X LINE
                .build();

At X line I get - Can't find symbol method setHeight. What is my mistake?

EDIT - I will have and a ExtendedDialogConfig that must extend DialogConfig and etc. I mean there will be other subclasses.

Upvotes: 1

Views: 796

Answers (2)

user5182503
user5182503

Reputation:

I found my mistake. This is how I used DialogConfigBuilder

DialogConfig config = new DialogConfig.DialogConfigBuilder()
                .setWidth(0)
                .setCalories(0)
                .setHeight(0) //X LINE
                .build();

This is how I should use DialogConfigBuilder

DialogConfig config = new DialogConfig.DialogConfigBuilder<>()
                .setWidth(0)
                .setCalories(0)
                .setHeight(0) //X LINE
                .build();

Pay attention to <> in the second case.

Upvotes: 1

GhostCat
GhostCat

Reputation: 140633

You would first change setCalories() to:

public Builder<B> setCalories(int calories) {
  this.calories = calories;
  return this;
}

to get rid of that cast and the warning. And now look at this closely. You return a Builder. This code doesn't know about future subclasses. It only returns an instance of that base builder.

As a consequence, when you have that chained call:

 .setHeight(0) .build();

that would return that base builder. To then call build() - which would build an abstract configuration. But you want to assign that to a more specific DialogConfig. Thus the error.

A (ugly) workaround:

DialogConfig.DialogConfigBuilder<?> builder = new DialogConfig.DialogConfigBuilder<>().setHeight(0);
builder.setCalories(0);

...config = builder.build();

And a solution - by again reworking setCalories():

@SuppressWarnings("unchecked")
public <T extends B> T setCalories(int calories) {
  this.calories = calories;
  return (T) this;
}

Fixes the compile error; and allows chaining the setCalories() call as well. Final exercise of getting rid of the cast/suppress is left as exercise to the reader.

And for the record - the "complete" solution, including all adaptions to get rid of raw types and other warnings:

abstract class AbstractConfig {
    public static abstract class Builder<B extends Builder<B>> {
        private int calories = 0;

        @SuppressWarnings("unchecked")
        public <T extends B> T setCalories(int calories) {
            this.calories = calories;
            return (T) this;
        }

        public abstract AbstractConfig build();
    }

    private int calories = 0;
    public int getCalories() { return calories; }

    protected <B extends Builder<B>> AbstractConfig(final Builder<B> builder) {
        calories = builder.calories;
    }
}

final class DialogConfig extends AbstractConfig {
    public static class DialogConfigBuilder<B extends DialogConfigBuilder<B>> extends Builder<B> {

        private double width;    
        private double height;

        public DialogConfigBuilder<B> setWidth(final double value) {
            width = value;
            return this;
        }

        public DialogConfigBuilder<B> setHeight(final double value) {
            height = value;
            return this;
        }

        public DialogConfig build() {
            return new DialogConfig(this);
        }
    }

    private final double width;
    private final double height;

    protected <B extends DialogConfigBuilder<B>> DialogConfig(final DialogConfigBuilder<B> builder) {
        super(builder);
        width = builder.width;
        height = builder.height;
    }

    public double getWidth() { return width; }
    public double getHeight() { return height; }
}

public class Builders {
    public static void main(String[] args) {
        DialogConfig config = new DialogConfig.DialogConfigBuilder<>().setHeight(0).setCalories(0).build();
        System.out.println(config);
    }
}

Upvotes: 1

Related Questions