3
\$\begingroup\$

I love pizza and design patterns. This is my first program to practice the builder pattern of GoF, without fluent interface. I put emphasis on the structure and characters of builder pattern.

Here are some notes, hope it will help you read:

  • Directors --> MeatPizzaLover, CheesePizzaLover. I add an extra-layer of abstraction, PizzaLover, to them.

  • Builder --> PizzaRecipe.

  • ConcreteBuilder --> CheesePizzaRecipe, MeatPizzaRecipe.

  • ComplexProduct --> Oven, Pizza, PrepareTips.

The PrepareTips is the result of a technique called Simple Factory. It's not a pattern but quite useful since it moves the variation part out of its client, the recipes.

There are a lot of points to improve. If you found anything that causes it not a builder pattern of GoF, put emphasis on it, thanks.

Maybe I should make two specific pizza types, CheesePizza and MeatPizza, but sorry, I didn't. I just want to focus on all the main characters in builder pattern. Hope this will not disturb your reading.


UML: Pizza Lover Builder Pattern By @N1ng only for CodeReview


Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
        PizzaLover pizzaLover;
        Pizza pizza;

        pizzaLover = new MeatPizzaLover();
        MeatPizzaRecipe meatPizzaRecipe = new MeatPizzaRecipe();
        pizzaLover.bakePizza(meatPizzaRecipe);
        pizza = meatPizzaRecipe.takeOutPizzaFromOven();
        System.out.println(pizza);

        pizzaLover = new CheesePizzaLover();
        CheesePizzaRecipe cheesePizzaRecipe = new CheesePizzaRecipe();
        pizzaLover.bakePizza(cheesePizzaRecipe);
        pizza = cheesePizzaRecipe.takeOutPizzaFromOven();
        System.out.println(pizza);
    }
}

Pizza.java

public class Pizza {
    private Size size;
    private String bacon;
    private String cheese;
    private String pepperoni;
    private List<String> toppings;
    public enum Size { NONE, PERSONAL, STANDARD, FAMILY }

    // Omit the default constructor

    // Setters & getters.
    public void setSize(Size size) {
        this.size = size;
    }

    public void setBacon(String bacon) {
        this.bacon = bacon;
    }

    public void setCheese(String cheese) {
        this.cheese = cheese;
    }

    public void setPepperoni(String pepperoni) {
        this.pepperoni = pepperoni;
    }

    public void setToppings(ArrayList<String> toppings) {
        this.toppings = (ArrayList<String>) toppings.clone();
    }

    public int getSize() {
        int inch = 10;

        switch (this.size) {
            case PERSONAL: inch = 10; break;
            case STANDARD: inch = 14; break;
            case FAMILY:   inch = 16; break;
            default:
        }
        return inch;
    }

    public String getBacon() {
        return this.bacon;
    }

    public String getCheese() {
        return this.cheese;
    }

    public String getPepperoni() {
        return this.pepperoni;
    }

    public List<String> getToppings() {
        return this.toppings;
    }

    public String toString() {
        StringBuilder result = new StringBuilder().append(getSize()).append("-inch pizza with ")
                .append(bacon).append(" ")
                .append(cheese).append(" ")
                .append(pepperoni);
        result.append(System.lineSeparator());
        for (String topping: this.toppings) {
            result.append(" + ").append(topping).append(System.lineSeparator());
        }
        return result.toString();
    }
}

PizzaLover.java

public abstract class PizzaLover {
    public abstract void bakePizza(PizzaRecipe pizzaRecipe) throws InterruptedException;
}

CheesePizzaLover.java

public class CheesePizzaLover extends PizzaLover {

    @Override
    public void bakePizza(PizzaRecipe pizzaRecipe) throws InterruptedException {
        System.out.println("In CheesePizzaLover.bakePizza():");
        pizzaRecipe.prepareIngredients();
        System.out.println("Set oven temperature to 400.0*F");
        pizzaRecipe.setOvenTemperature(400.0);
        System.out.println("Heat the oven about 2 second...");
        pizzaRecipe.setTimeOfOvenTimer(2);

        if (pizzaRecipe.getOvenTemperature() < 300.0) {
            System.out.println("Since oven temperature less than 300.0*F, re-heat the oven for 2-sec...");
            pizzaRecipe.setTimeOfOvenTimer(2);
        }

        System.out.println("Stretching the pizza dough twice to make it STANDARD size.");
        pizzaRecipe.stretchOutDough();
        pizzaRecipe.stretchOutDough();


        pizzaRecipe.addSauce();
        pizzaRecipe.addToppings();
        pizzaRecipe.addMeat();
        System.out.println("Adding cheese twice.");
        pizzaRecipe.addCheese();
        pizzaRecipe.addCheese();
        pizzaRecipe.putPizzaInOven();
    }

}

MeatPizzaLover.java

public class MeatPizzaLover extends PizzaLover {

    @Override
    public void bakePizza(PizzaRecipe pizzaRecipe) throws InterruptedException {
        System.out.println("In MeatPizzaLover.bakePizza():");
        pizzaRecipe.prepareIngredients();
        System.out.println("Set oven temperature to 300.0*F");
        pizzaRecipe.setOvenTemperature(300.0);
        System.out.println("Heat the oven about 5 seconds...");
        pizzaRecipe.setTimeOfOvenTimer(5);

        if (pizzaRecipe.getOvenTemperature() < 500.0) {
            System.out.println("Since oven temperature less than 500.0*F, re-heat the oven for 2-sec...");
            pizzaRecipe.setTimeOfOvenTimer(2);
        }

        System.out.println("Stretching the pizza dough three times to make it FAMILY size.");
        pizzaRecipe.stretchOutDough();
        pizzaRecipe.stretchOutDough();
        pizzaRecipe.stretchOutDough();

        pizzaRecipe.addSauce();
        pizzaRecipe.addToppings();
        System.out.println("Adding meat twice.");
        pizzaRecipe.addMeat();
        pizzaRecipe.addMeat();
        pizzaRecipe.addCheese();
        pizzaRecipe.putPizzaInOven();
    }

}

PizzaRecipe.java

public interface PizzaRecipe {
    public void prepareIngredients();
    public void setOvenTemperature(double fahrenheit);
    public double getOvenTemperature();
    public void setTimeOfOvenTimer(int second) throws InterruptedException;
    public void stretchOutDough();
    public void addSauce();
    public void addToppings();
    public void addMeat();
    public void addCheese();
    public void putPizzaInOven() throws InterruptedException;
}

CheesePizzaRecipe.java

public class CheesePizzaRecipe implements PizzaRecipe {
    private Pizza pizza = new Pizza();
    private PrepareTips prepareTips = new PrepareTips();
    private Oven oven = new Oven();
    private Pizza.Size currentPizzaSize = Pizza.Size.NONE;

    @Override
    public void prepareIngredients() {
        prepareTips.prepareDoughTips();
    }

    @Override
    public void setOvenTemperature(double fahrenheit) {
        oven.setTemperature(fahrenheit);
    }

    @Override
    public double getOvenTemperature() {
        return oven.getTemperature();
    }

    @Override
    public void setTimeOfOvenTimer(int second) throws InterruptedException {
        oven.setTimer(second);
    }

    @Override
    public void stretchOutDough() {
        prepareTips.stretchDoughTips();
        switch (currentPizzaSize) {
            case PERSONAL: currentPizzaSize = Pizza.Size.STANDARD; break;
            case STANDARD: currentPizzaSize = Pizza.Size.FAMILY;   break;
            case FAMILY:   break;
            default:       currentPizzaSize = Pizza.Size.PERSONAL; break;
        }
        pizza.setSize(currentPizzaSize);
    }

    @Override
    public void addSauce() {
        prepareTips.addSauceTips();
    }

    @Override
    public void addToppings() {
        List<String> myToppings = Arrays.asList("Mushrooms", "Onions", "Green peppers", "Pineapple");
        pizza.setToppings(new ArrayList<>(myToppings));
    }

    @Override
    public void addMeat() {
        pizza.setBacon("Uncured Belly Rashers(bara)");
        pizza.setPepperoni("Peppers Pepperoni");
    }

    @Override
    public void addCheese() {
        pizza.setCheese("Mozzarella with Extra Cheese: Cheddar, Provolone, Grated Parmesan");
    }

    @Override
    public void putPizzaInOven() throws InterruptedException {
        System.out.println("Final 10-sec baking of cheese pizza...");
        oven.setTimer(10);
    }

    public Pizza takeOutPizzaFromOven() {
        return pizza;
    }
}

MeatPizzaRecipe.java

public class MeatPizzaRecipe implements PizzaRecipe {
    private Pizza pizza = new Pizza();
    private PrepareTips prepareTips = new PrepareTips();
    private Oven oven = new Oven();
    private Pizza.Size currentPizzaSize = Pizza.Size.NONE;

    @Override
    public void prepareIngredients() {
        prepareTips.prepareDoughTips();
    }

    @Override
    public void setOvenTemperature(double fahrenheit) {
        oven.setTemperature(fahrenheit);
    }

    @Override
    public double getOvenTemperature() {
        return oven.getTemperature();
    }

    @Override
    public void setTimeOfOvenTimer(int second) throws InterruptedException {
        oven.setTimer(second);
    }

    @Override
    public void stretchOutDough() {
        prepareTips.stretchDoughTips();
        switch (currentPizzaSize) {
            case PERSONAL: currentPizzaSize = Pizza.Size.STANDARD; break;
            case STANDARD: currentPizzaSize = Pizza.Size.FAMILY;   break;
            case FAMILY:   break;
            default:       currentPizzaSize = Pizza.Size.PERSONAL; break;
        }
        pizza.setSize(currentPizzaSize);
    }

    @Override
    public void addSauce() {
        prepareTips.addSauceTips();
    }

    @Override
    public void addToppings() {
        List<String> myToppings = Arrays.asList("Mushrooms", "Onions", "Black olives", "Pineapple");
        pizza.setToppings(new ArrayList<>(myToppings));
    }

    @Override
    public void addMeat() {
        pizza.setBacon("Roasted Peameal Bacon");
        pizza.setPepperoni("Garlic Pepperoni");
    }

    @Override
    public void addCheese() {
        pizza.setCheese("Mozzarella");
    }

    @Override
    public void putPizzaInOven() throws InterruptedException {
        System.out.println("Final 5-sec baking of meat pizza...");
        oven.setTimer(5);
    }

    public Pizza takeOutPizzaFromOven() {
        return pizza;
    }
}

PrepareTips.java

public class PrepareTips {
    public static void appendWithNewLine(StringBuilder stringBuilder, String string) {
        stringBuilder.append(string);
        stringBuilder.append(System.lineSeparator());
    }

    public void prepareDoughTips() {
        StringBuilder tips = new StringBuilder();
        appendWithNewLine(tips, "Taking out the pizza dough from the refrigerator...");
        appendWithNewLine(tips, "Covering it and let it warm up to room temperature...");
        appendWithNewLine(tips, "Covering the dough keeps the surface from drying out and forming a crust...");
        System.out.print(tips.toString());
    }

    public void stretchDoughTips() {
        StringBuilder tips = new StringBuilder();
        appendWithNewLine(tips, "Stretching and flattening the dough on a floured work surface...");
        System.out.print(tips.toString());
    }

    public void addSauceTips() {
        StringBuilder tips = new StringBuilder();
        appendWithNewLine(tips, "Spreading the sauce evenly over the dough...");
        System.out.print(tips.toString());
    }
}

Oven.java

public class Oven {
    private double temperature;

    public void setTemperature(double temperature) {
        this.temperature = temperature;
    }

    public double getTemperature() {
        return this.temperature;
    }

    public void setTimer(int sec) throws InterruptedException {
        Thread.sleep(sec * 1000);
        System.out.println("Ding!");
    }
}

Output:

In MeatPizzaLover.bakePizza():
Taking out the pizza dough from the refrigerator...
Covering it and let it warm up to room temperature...
Covering the dough keeps the surface from drying out and forming a crust...
Set oven temperature to 300.0*F
Heat the oven about 5 seconds...
Ding!
Since oven temperature less than 500.0*F, re-heat the oven for 2-sec...
Ding!
Stretching the pizza dough three times to make it FAMILY size.
Stretching and flattening the dough on a floured work surface...
Stretching and flattening the dough on a floured work surface...
Stretching and flattening the dough on a floured work surface...
Spreading the sauce evenly over the dough...
Adding meat twice.
Final 5-sec baking of meat pizza...
Ding!
16-inch pizza with Roasted Peameal Bacon Mozzarella Garlic Pepperoni
 + Mushrooms
 + Onions
 + Black olives
 + Pineapple

In CheesePizzaLover.bakePizza():
Taking out the pizza dough from the refrigerator...
Covering it and let it warm up to room temperature...
Covering the dough keeps the surface from drying out and forming a crust...
Set oven temperature to 400.0*F
Heat the oven about 2 second...
Ding!
Stretching the pizza dough twice to make it STANDARD size.
Stretching and flattening the dough on a floured work surface...
Stretching and flattening the dough on a floured work surface...
Spreading the sauce evenly over the dough...
Adding cheese twice.
Final 10-sec baking of cheese pizza...
Ding!
14-inch pizza with Uncured Belly Rashers(bara) Mozzarella with Extra Cheese: Cheddar, Provolone, Grated Parmesan Peppers Pepperoni
 + Mushrooms
 + Onions
 + Green peppers
 + Pineapple
\$\endgroup\$
0
5
\$\begingroup\$

I think you model the domain wrong.

A recipe is a list of instructions with a list of ingredients. What kind of recipe it is does not matter. The "cheeseyness" of a recipe does not matter for the class.

So I think there should only be a Recipe class, of which there are two instances, namely a meatLoversPizzaRecipe, and a cheeseLoversPizzaRecipe.

Consider modeling Instruction (as abstract) and Ingredient (as enum?) as well - that way you could make something like an AddToppingInstruction. I don't wanna go to deep with the recipes, so we'll keep it simple on that front.

Looking more at the diagram, I also think that the current program doesn't work with a user. The user does not get to decide what pizza they want. Pizza lovers are forced to make a pizza they love only to have it taken away. This doesn't make the pizza lover or the user happy.

We'll set out to do the following:

  1. Allow the user to decide what pizza they want
  2. Make proper use of the builder pattern
  3. Take a look at your existing code

In order to avoid serving customer unfinished pizza's (and to have the whole builder pattern make sense to use, since it's rather hefty), we should make it so that all Pizza's are built and baked. That is to say, a Pizza is done after instantiation.

public class Pizza {
    private Size size;
    private String bacon;
    private String cheese;
    private String pepperoni;
    private List<String> toppings;

Your current Pizza isn't a finished pizza, it's a pizza that's in a certain state of construction.

Consider making a Pizza that does not have setters, just getters (to examine the Pizza) and action methods (to eat the pizza).

You can reuse your current Pizza class as an UnbakedPizza class.

Create a new PizzaBuilder class which has the job to, well, make Pizza's. They're instantiated with an Oven, and will give Pizza's when handed a Recipe.

I've written parts of a PizzaBuilder here to give you an idea how a Builder class would work.

public class PizzaBuilder {
    private Oven oven;

    //... constructor 

    public Pizza makePizza(PizzaRecipe recipe) {
        //perhaps a recipe needs parsing, you could explode recipe into instructions here
        PizzaBase base = makeBase(recipe);
        UnbakedPizza rawPizza = applyToppings(recipe, base);
        Pizza bakedPizza = cookPizza(rawPizza);
        return bakedPizza;//perhaps we could add seasonings to bakedPizza?
    }

    private PizzaBase makeBase(PizzaRecipe recipe) {
        //Maybe you have to convert the recipe to other values here?
        return new PizzaBase(recipe.getSize(), recipe.getDoughType());
    }

    private UnbakedPizza applyToppings(PizzaRecipe recipe, PizzaBase base) {
        List<String> toppings = recipe.getToppings();
        //special pizza making business logic goes here
        return new UnbakedPizza(base, toppings);
    }

    private Pizza bakePizza(PizzaRecipe recipe, UnbakedPizza rawPizza) {
        prepareOven(recipe);//hey look, pizza business logic
        oven.add(rawPizza);
        //waiting happens
        return oven.retrieve();
    }
}

The goal of a builder class is to hide all/most the object creation logic, which may involve filling defaults, defining buffers etc, from the client.

So, how do you use this builder?

Part of a useful builder is the ability to either tell the builder, or another related object that the builder accepts, what you want from the builder. That's where the PizzaRecipe comes in.

public class MeatPizzaLover extends PizzaLover {

    @Override
    public void bakePizza(PizzaRecipe pizzaRecipe) throws InterruptedException {
        System.out.println("In MeatPizzaLover.bakePizza():");
        pizzaRecipe.prepareIngredients();
        System.out.println("Set oven temperature to 300.0*F");
        pizzaRecipe.setOvenTemperature(300.0);
        System.out.println("Heat the oven about 5 seconds...");
        pizzaRecipe.setTimeOfOvenTimer(5);

        if (pizzaRecipe.getOvenTemperature() < 500.0) {
            System.out.println("Since oven temperature less than 500.0*F, re-heat the oven for 2-sec...");
            pizzaRecipe.setTimeOfOvenTimer(2);
        }

        System.out.println("Stretching the pizza dough three times to make it FAMILY size.");
        pizzaRecipe.stretchOutDough();
        pizzaRecipe.stretchOutDough();
        pizzaRecipe.stretchOutDough();

        pizzaRecipe.addSauce();
        pizzaRecipe.addToppings();
        System.out.println("Adding meat twice.");
        pizzaRecipe.addMeat();
        pizzaRecipe.addMeat();
        pizzaRecipe.addCheese();
        pizzaRecipe.putPizzaInOven();
    }

}

This would be one of your consumers. They're doing... mostly the right stuff, but on too low an abstraction level. I think they're on the wrong side of the counter! All this stretching, adding ingredients, and baking is part of the PizzaBuilder's job!

The PizzaLover, instead, as the client, defines the recipe.

//pseudo code
@Override
public Pizza orderPizza() {
    PizzaRecipe recipe = new PizzaRecipe();
    recipe.setSize(FAMILY);

    recipe.addTopping("cheese");
    recipe.addTopping("meat");

    recipe.setBakeTime(5);
    recipe.setTemperature(300);

    return Pizzeria.getBaker().bakePizza(recipe);
}

Think about how Dominoes and other pizza ordering places work. You can pick a size, a bunch of toppings, and this "recipe" is sent to the Pizzeria. You personally don't have to know all the details to get delicious pizza.

I'd even say that in the pseudo code example, the bake time and temperature should be based on dough type and size (pizza business logic!) and not something the customer provides. How is a customer supposed to know how long they want their pizza baked? They just want a "regular dough medium sized meat lovers pizza".

Anyway, I think I've rambled on enough about how I think you could model the Builder pattern better with pizza. Let's take a look at the code.


public class Pizza {
    private Size size;
    private String bacon;
    private String cheese;
    private String pepperoni;
    private List<String> toppings;
    public enum Size { NONE, PERSONAL, STANDARD, FAMILY }


public int getSize() {
    int inch = 10;

    switch (this.size) {
        case PERSONAL: inch = 10; break;
        case STANDARD: inch = 14; break;
        case FAMILY:   inch = 16; break;
        default:
    }
    return inch;
}

I think that getSize could be part of the Size enumeration. Right now, the Pizza acts as a size-to-inches converter, which is kinda like ordering a personal sized pizza and then only once you receive it, finding out it's kinda tiny. You don't want a tiny pizza. If you make getSize part of the Size enumeration, you could display the inches and words in a menu:

System.out.println("Which size do you want?");
for (Size size : Size.values()) {
    if (size != Size.NONE) {
        System.out.println(size.toString() + " - " + size.getInches() + " inches");
    }
}

public void setToppings(ArrayList<String> toppings) {
    this.toppings = (ArrayList<String>) toppings.clone();
}

If you need to clone a list, just make a new list. Additionally, why do you need an ArrayList? Just List will do - you don't care that toppings are neatly into an array, just that they're ordered in some way so your pepperoni isn't buried under the sauce and cheese.

public void setToppings(List<String> toppings) {
    this.toppings = new ArrayList<>(toppings);
}

public String toString() {
    StringBuilder result = new StringBuilder().append(getSize()).append("-inch pizza with ")
            .append(bacon).append(" ")
            .append(cheese).append(" ")
            .append(pepperoni);
    result.append(System.lineSeparator());
    for (String topping: this.toppings) {
        result.append(" + ").append(topping).append(System.lineSeparator());
    }
    return result.toString();
}

This toString() assumes pizza's always have bacon, cheese and pepperoni. Consider treating cheese, bacon and pepperoni as toppings (four cheese pizza? How would you currently model it?). You can then make use of streams and Collectors.joining:

public String toString() {
    String base = getSize() + "-inch pizza with ";
    return this.toppings.stream()
           .collect(Collectors.joining(System.lineSeperator() + " + ", base, System.lineSeparator()));
}

The last issue in your code I see is that InterruptedException is being defined as a checked throw everywhere. It's leaking.

I'd recommend that you catch it in the Oven for now. Later, you'll want to move baking a pizza to a separate thread, so that the PizzaBaker can take and work on multiple orders whilst waiting for the Pizza's that are in the oven.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.