parsecer
parsecer

Reputation: 5176

Java builder classes generics hierarchy: how to make build() return correct method

I have this class structure:

                Stuff
              /       \
   Food
     |   
   FastFood                  Drink
     |                        |
   Pizza                    Juice
  /     \                   /    \
 |     SicilianPizza       |    OrangeJuice
 |                      AppleJuice
CaliforniaPizza

I can create pizza and juice objects using builders like this:

CaliforniaPizza californiaPizze = CaliforniaPizza.builder(2)
                .addTopping("Tomatoes").addOlives(true).build();
        SicilianPizza sicilianPizza = SicilianPizza.builder(1)
                .addTopping("Bacon").addCheese(false).build();

        AppleJuice appleJuice = AppleJuice.builder(40)
                .setPrice(120).setAlcoholVolume(0).setAppleColor("yellow").build();
        OrangeJuice orangeJuice = OrangeJuice.builder(35).setOrangeSize(8).build();

Every setter method of the builder returns correct object:

enter image description here

I'd like to create a method that would accept a FastFood.Builder object and use it to create a FastFood object that would pass it to another method:

public void makeFat(FastFood fastFood)  {...}

But I can't: setter methods don't return FastFood, but rather its parents:

enter image description here

Is there something wrong with my generics? How could that be accomplished?

package pizza;

import java.util.*;

class TestStuff {
    public static void main(String[] args) {
        CaliforniaPizza californiaPizze = CaliforniaPizza.builder(2)
                .addTopping("Tomatoes").addOlives(true).build();
        SicilianPizza sicilianPizza = SicilianPizza.builder(1)
                .addTopping("Bacon").addCheese(false).build();

        AppleJuice appleJuice = AppleJuice.builder(40)
                .setPrice(120).setAlcoholVolume(0).setAppleColor("yellow").build();
        OrangeJuice orangeJuice = OrangeJuice.builder(35).setOrangeSize(8).build();
    }

    public FastFood testFood(FastFood.Builder fastFoodBuilder)  {
       //return  fastFoodBuilder.setMealType("fd").addOlives(true).setPrice(20).build();
    }

    public void makeFat(FastFood fastFood)  {}
}



abstract class Stuff {
    protected double price;

    protected Stuff()  {}

    protected abstract class Builder<T extends Builder<T>>  {
        protected abstract Stuff build();
        protected abstract T self();

        public T setPrice(double price)  {
            Stuff.this.price = price;
            return self();
        }
    }
}

abstract class Food extends Stuff {
    protected String mealType; //breakfast/dinner/etc

    protected Food()  {}

    public abstract class Builder<T extends Builder<T>> extends Stuff.Builder<T> {
        protected abstract Food build();
        protected abstract T self();

        public T setMealType(String mealType)  {
            Food.this.mealType = mealType;
            return self();
        }
    }
}

abstract class FastFood extends Food  {
    protected int harm;

    protected FastFood() {}

    public abstract class Builder<T extends Builder<T>> extends Food.Builder<T> {
        protected abstract FastFood build();
        protected abstract T self();

        public T setHarm(int harm)  {
            FastFood.this.harm = harm;
            return self();
        }
    }
}

abstract class Pizza extends FastFood {
    protected List<String> toppings = new ArrayList<>();  //optional
    protected int size;  //obligatory

    protected Pizza(int size)  {this.size = size;}

    public abstract class Builder<T extends Builder<T>> extends FastFood.Builder<T>  {
        public T addTopping(String topping)  {
            toppings.add(topping);
            return self();
        }
    }
}

class CaliforniaPizza extends Pizza {
    private boolean addOlives;

    private CaliforniaPizza(int size) {super(size);}

    public static Builder builder(int size)  {return new CaliforniaPizza(size).new Builder();}

    public class Builder extends Pizza.Builder<Builder>  {
        @Override
        public CaliforniaPizza build()  {
            return CaliforniaPizza.this;
        }

        @Override
        public Builder self()  {return this;}

        public Builder addOlives(boolean addOlives)  {
            CaliforniaPizza.this.addOlives = addOlives;
            return this;
        }
    }
}

class SicilianPizza extends Pizza {
    private boolean addCheese;

    private SicilianPizza(int size)  {super(size);}

    public static Builder builder(int size)  {
        return new SicilianPizza(size).new Builder();
    }

    public class Builder extends Pizza.Builder<Builder>  {
        @Override
        public SicilianPizza build()  {return SicilianPizza.this;}

        @Override
        public Builder self()  {return this;}

        public Builder addCheese(boolean addCheese)  {
            SicilianPizza.this.addCheese = addCheese;
            return this;
        }
    }
}

abstract class Drink extends Stuff {
    protected double density;
    protected double alcoholVolume;

    protected Drink(double density)  {this.density = density;}

    public abstract class Builder<T extends Builder<T>> extends Stuff.Builder<T>  {
        protected abstract Drink build();
        protected abstract T self();

        public T setAlcoholVolume(double alcoholVolume)  {
            Drink.this.alcoholVolume = alcoholVolume;
            return self();
        }
    }
}

abstract class Juice extends Drink {
    private String color;

    protected Juice(double density)  {super(density);}

    public abstract class Builder<T extends Builder<T>> extends Drink.Builder<T>  {
        public Builder<T> setColor(String color)  {
            Juice.this.color = color;
            return self();
        }
    }
}
class AppleJuice extends Juice {
    private String appleColor;

    private AppleJuice(double density)  {super(density);}

    public static Builder builder(double density)  {return new AppleJuice(density).new Builder();}

    public class Builder extends Juice.Builder<Builder>  {
        @Override
        public AppleJuice build()  {
            return AppleJuice.this;
        }

        @Override
        public Builder self()  {
            return this;
        }

        public Builder setAppleColor(String appleColor)  {
            AppleJuice.this.appleColor = appleColor;
            return this;
        }
    }
}

class OrangeJuice extends Juice{
    private int orangeSize;

    private OrangeJuice(double density)  {super(density);}

    public static Builder builder(double density)  {return new OrangeJuice(density).new Builder();}

    public class Builder extends Juice.Builder<Builder>  {
        @Override
        public OrangeJuice build()  {return OrangeJuice.this;}

        @Override
        public Builder self()  {return this;}

        public Builder setOrangeSize(int orangeSize)  {
            OrangeJuice.this.orangeSize = orangeSize;
            return this;
        }
    }
}

Upvotes: 0

Views: 183

Answers (2)

Eugene Khyst
Eugene Khyst

Reputation: 10315

There are few problems with the code.

Class FastFood.Builder is generic: class Builder<T extends Builder<T>> extends Food.Builder<T>.

In the method FastFood testFood(FastFood.Builder fastFoodBuilder) the builder is declared without specifying the type T.

For example, AppleJuice specifies the parameter T type: class Builder extends Juice.Builder<Builder>.

The same problem will be present for Drink.Builder:

public static Drink testDrink(Drink.Builder drinkBuilder) {
    return drinkBuilder
            .setPrice(100) // returns Stuff.Builder
            .setAlcoholVolume(0.7) //Error: cannot resolve method
            .build();
}

To fix this, make method testFood generic accepting parameter FastFood.Builder<T>:

public static <T extends FastFood.Builder<T>> FastFood testFood(FastFood.Builder<T> fastFoodBuilder) {
    return fastFoodBuilder.setMealType("fd").setHarm(1).setPrice(20).build();
}

The same is true for methods accepting Drink.Builder:

public static <T extends Drink.Builder<T>> Drink testDrink(Drink.Builder<T> drinkBuilder) {
    return drinkBuilder.setPrice(100).setAlcoholVolume(0.7).build();
}

Usage example:

FastFood californiaPizza1 = testFood(CaliforniaPizza.builder(2));

In order to call addOlives(boolean) method and avoid casting from FastFood to a concrete class (e.g. CaliforniaPizza), it's more convenient to return builder from a method:

public static <T extends FastFood.Builder<T>> T testFoodBuilder(FastFood.Builder<T> fastFoodBuilder) {
    return fastFoodBuilder.setMealType("fd").setHarm(1).setPrice(20);
}

And the usage:

CaliforniaPizza californiaPizza2 = 
            testFoodBuilder(CaliforniaPizza.builder(2))
                    .addOlives(true)
                    .build();

Upvotes: 1

Tom Hawtin - tackline
Tom Hawtin - tackline

Reputation: 147164

You have the line:

public FastFood testFood(FastFood.Builder fastFoodBuilder) {

But also:

abstract class FastFood extends Food  {
    ...
    public abstract class Builder<T extends Builder<T>> extends Food.Builder<T> {

There should be a warning saying that FastFood.Builder is a generic type.

The parameter type needs to be made appropriately generic. A wildcard will do (constraints will be inferred).

public FastFood testFood(FastFood.Builder<?> fastFoodBuilder) {

Upvotes: 1

Related Questions