mercredi 20 novembre 2019

Nested generic types in Builder pattern: how to implement and avoid type erasure (ascii tree, pictures)

I have the following class hierarchy:

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

Each class has a field(s) that should be set, some of those fields are obligatory (ob) and must be passed, some are optional (op) and can be set via a builder:

Stuff -> double price (op)
  Food -> String mealType (op)
    Pizza -> List<String> toppings (op), int size (ob)
      CaliforniaPizza -> boolean addOlives (op)
      SicilianPizza -> boolean addCheese (op)
  Drink -> double density (ob), double alcoholVolume (op)
    Juice -> String color (op)
      AppleJuice -> String appleColor (op)
      OrangeJuice -> int orangeSize (op)

I use builders to build the pizzas and drinks:

TestStuff.java:

public 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();
    }
}

These are my classes:

Stuff.java:

public abstract class Stuff {
    protected double price;

    protected Stuff()  {}

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

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

Food.java:

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

    protected Food()  {}

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

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

Pizza.java:

public abstract class Pizza extends Food {
    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> extends Food.Builder<Builder>  {
        public T addTopping(String topping)  {
            toppings.add(topping);
            return (T) self();
        }
    }
}

CaliforniaPizza.java:

public 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;
        }
    }
}

SicilianPizza.java:

public 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;
        }
    }
}

Drink.java:

public 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> extends Stuff.Builder<Builder>  {
        protected abstract Drink build();
        protected abstract T self();

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

Juice.java:

public abstract class Juice extends Drink {
    private String color;

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

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

AppleJuice.java:

public 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;
        }
    }
}

OrangeJuice.java:

public 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;
        }
    }
}

I have several problems with this code:

  1. @Override annotations above build() and self inside CaliforniaPizza and SicilianPizza are highlighted in Idea Intellij and the error message says that method does not override method from its super class. While there are no such errors inside Juice child classes.
  2. This line inside TestStuff:
 AppleJuice appleJuice = AppleJuice.builder(40)                    
      .setPrice(120).setAlcoholVolume(0)
      .setAppleColor("yellow").build();

gives error: setAppleColor is highlited with red and can't be resolved. The reason is that setAlcoholVolume returns a Drink.Builder instead of AppleJuice.Builder. At the same time if I were to call setAlcoholVolume right after builder() method it would return Juice.Builder:

enter image description here

  1. Although there are problems with juices builders, at least I can somewhat access all the methods (at least if I call them right after builder(). However with foods my options are limited:

enter image description here

There's not Food's setMealType there.

  1. I have a return (T) self() line inside setters in Juice and Pizza. However, only inside Juice do I get the warning about unchecked cast:

enter image description here

while in Pizza I get no warning at all.

I have suspicions that at least problems 2 and 3 have to do with type erasure. An AppleJuice.Builder gets passed to Juice.Builder as T, but then the Juice.Builder<AppleJuice.Builder> gets passed to Drink.Builder<Juice.Builder> and the information about AppleJuice gets lost. Then Drink.Builder<Juice.Builder> gets passed to Stuff.Builder<Drink.Builder> and only information about Drink.Builder is retained. Thus when I call setPrice(120) right away, it returns Drink.Builder instead of Juice.Builder. If I'm correct, what is the way to fix it? And what are the reasons behind another issues I've encountered?

Aucun commentaire:

Enregistrer un commentaire