3

If I am using the builder pattern to configure new objects I may have two classes like Game and HockeyGame (shown below). When I want to create a new HockeyGame, I get it's builder and start calling methods to configure the object as needed.

The problem I am running into is shown in the main function. Once I call one method from the super class it returns as an intance of Game.Builder, and I can no longer call any method from the child class.

What is the best way to deal with this?

Main.java

class Main {

    public static void main(String[] args){

        HockeyGame hg = new HockeyGame.Builder()
                .setScore(5)
                .setTimeLimit(3600)
//--------------------------------------------------------------------
                .setIceTemperature(-5) // Error! Cannot call setIceTempurature() on
                                       // an instance of Game.Builder
//--------------------------------------------------------------------
                .build();


    }
}

Game.java

public class Game{

    int score;
    int timeLimit;

    public Game(int score, int timeLimit) {
        this.score = score;
        this.timeLimit = timeLimit;
    }

    public static class Builder {

        int score;
        int timeLimit;

        public Builder setScore(int score) {
            this.score = score;
            return this;
        }

        public Builder setTimeLimit(int timeLimit) {
            this.timeLimit = timeLimit;
            return this;
        }

        public Game build() {
            return new Game(score, timeLimit);
        }
    }
}

HockeyGame.java

public class HockeyGame extends Game {

    float iceTemperature;

    public HockeyGame(int score, int timeLimit, float iceTemperature) {
        super(score, timeLimit);
        this.iceTemperature = iceTemperature;
    }

    public static class Builder extends Game.Builder {

        float iceTemperature;

        public HockeyGame.Buidler setIceTemperature(float iceTemperature) {
            this.iceTemperature = iceTemperature;
            return this;
        }

        public HockeyGame build(){
            return new HockeyGame(score, timeLimit, iceTemperature);
        }
    }
}

Thanks.

2 Answers2

5

You need to use the getThis() trick that is prevalent in much fluent API code.

First you need to make your Game.Builder generic in itself:

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

Then you add a getThis() method:

public B getThis() {
    return (B) this;
}

Now you change your setters to return a B and return getThis() rather than this:

public B setTimeLimit(int timeLimit) {
    //...
    return getThis();
}

Your extension class also needs to be generic, in itself:

public static class Builder<B extends Builder<B>> extends Game.Builder<B>

Now you can use the code, and it will "remember" the intended type:

HockeyGame hockeyGame = new HockeyGame.Builder<>().setScore(10)
        .setTimeLimit(20)
        .setIceTemperature(-1)
        .build();

This final code looks something like:

public class Game {

    private final int score;
    private final int timeLimit;

    private Game(final Builder<?> builder) {
        this.score = builder.score;
        this.timeLimit = builder.timeLimit;
    }

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

        private int score;
        private int timeLimit;

        public B setScore(int score) {
            this.score = score;
            return getThis();
        }

        public B setTimeLimit(int timeLimit) {
            this.timeLimit = timeLimit;
            return getThis();
        }

        protected B getThis() {
            return (B) this;
        }

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

public class HockeyGame extends Game {

    private final float iceTemperature;

    private HockeyGame(final Builder<?> builder) {
        super(builder);
        this.iceTemperature = builder.iceTemperature;
    }

    public static class Builder<B extends Builder<B>> extends Game.Builder<B> {

        private float iceTemperature;

        public B setIceTemperature(float iceTemperature) {
            this.iceTemperature = iceTemperature;
            return getThis();
        }

        @Override
        public HockeyGame build() {
            return new HockeyGame(this);
        }
    }
}

N.B: I have made the fields private final and also the main type constructors - this forces people to use the Builder. Also, the constructor can take a Builder<?> and copy the variable from there - this tidies the code a little.


The actual hack is, as you may have noticed, here:

public B getThis() {
    return (B) this;
}

Here, we force a cast of the Builder to its generic type - this allows us to change the return type of the method dependant upon the specific instance being used. The issue is, if you declare a new Builder something like the following:

public static class FootballGame extends Game {

    private FootballGame(final Builder<?> builder) {
        super(builder);
    }

    public static class Builder<B extends HockeyGame.Builder<B>> extends Game.Builder<B> {

        float iceTemperature;

        @Override
        public FootballGame build() {
            return new FootballGame(this);
        }
    }
}

This this will blow up at runtime with a ClassCastException. But the setter method will return a HockeyGame.Builder rather than FootballGame.Builder so the issue should be obvious.

Boris the Spider
  • 59,842
  • 6
  • 106
  • 166
0

Try something like this

You explicitely cast it back to a HockeyGame.Builder object and work with its own method(s) on it.

The problem you had is that setTimeLimit returns a Builder object (mother class) and so you can not use the child methods on it.

HockeyGame hg = ((HockeyGame.Builder)(new HockeyGame.Builder().setScore(5)
                                                              .setTimeLimit(3600)))
                                                              .setIceTemperature(-5)
                                                              .build();

Also, setIceTemparature should return a HockeyGame.Builder object to be able to build on it.

public Builder setIceTemperature(float iceTemperature) {
    this.iceTemperature = iceTemperature;
    return this;
}
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89