Starter files: code.zip
The objectives of this lab are:
Use the builder pattern to help create objects in a multi-step process.
Use the builder pattern to help write immutable objects.
Compare and contrast designs with and without builders.
Note: the final design in this lab is inspired from the book Effective Java by Joshua Bloch.
Instantiating an object is one of the basic operations in object-oriented programming. However there are several situations where this basic operation is complicated and error-prone. In other situations we are faced with objects that should not be mutable, but have to be in order to customize them before use. In this lab we will use the builder pattern to address these issues in a given design.
The provided starter code defines several classes to represent pizzas. A pizza
is represented by the
Pizza interface, and implemented by the
AlaCartePizza class. This class is a general-purpose implementation that
can be used to create pizzas with different crusts, sizes and an arbitrary
number of toppings (the “build-your-own” pizza). Two other classes
VeggiePizza) represent specific kinds of pizzas
(fixed menu options). A simple test class has also been provided, that shows
how to create different pizzas and determine their cost.
Please create a new project, load in these files, verify that the test runs successfully and read through the provided code to understand the design.
Your local pizzeria store hired a programmer to implement their system, and
this code was part of it. You inherited this code when you were hired by
them. You understood that the
methods allowed a client to customize toppings when assembling the pizza. But
you also found a flaw: a pizza could still be modified after its assembly is
“complete”. For example, a
Pizza object can be assembled using these
methods, and then passed to a method that may modify it using the same
methods. This should not be allowed.
Your first instinct was to replace these two methods with a constructor that takes in the crust type, size and a list of toppings. Although this would prevent unwanted mutation, you concluded that such a constructor may be cumbersome and error-prone. For example, one would need to assemble all toppings in a list before calling the constructor. This assembly would be more commplicated if pizza creation is an interactive process.
In this lab, you will use the builder design pattern to address this issue.
We first remove the possibility of mutating a pizza.
Create a new package called betterpizza. This package will contain a “better” version of this design.
Create a new interface called
ObservablePizzain this package. This interface will contain only those methods from the
Pizzainterface that do not mutate (cost and hasTopping). Copy their signatures and documentation exactly from the given
Pizzainterface into the new
Remove these two methods from the
Pizzainterface, and make the
Pizzainterface extend the
Create a copy of the provided
AlaCartePizzaclass in the betterpizza package (keep the name of the class the same). We will modify it in the next part.
In this change we have refactored the
Pizza interface without changing
the methods it declares. This means that all existing implementations of the
Pizza interface will remain unchanged, even though we segregated the
functionality into operations that mutate and operations that observe.
We now re-engineer the new
AlaCartePizza class kept in the
betterpizza package, to disallow mutation and facilitating a multi-step
pizza assembly process. We achieve this by using the Builder design pattern.
All the changes in the remainder of this lab apply to the classes in the
betterpizza package. All provided code should continue to remain in the
Modify the code so that the (new)
AlaCartePizzaclass implements the
ObservablePizzainterface from the same package.
publicmethods from the (new)
AlaCartePizzaclass that are not present in the
Create a new abstract class named
betterpizzapackage. This class represents a pizza builder.
Add a public, static inner class called
AlaCartePizzaclass. This builder class should extend the
Add a new protected constructor in the (new)
AlaCartePizzathat takes in the size, crust and a map of toppings to direct set its fields. It should throw IllegalArgumentException objects if either of the parameters are null. We will call this constructor from the builder.
Our end goal is to allow creating the equivalent of the pizza created in the given test class, using code like this:
ObservablePizza alacarte = new AlaCartePizza.AlaCartePizzaBuilder() .crust(Crust.Classic) .size(Size.Medium) .addTopping(ToppingName.Cheese, ToppingPortion.Full) .addTopping(ToppingName.Sauce,ToppingPortion.Full) .addTopping(ToppingName.GreenPepper,ToppingPortion.Full) .addTopping(ToppingName.Onion,ToppingPortion.Full) .addTopping(ToppingName.Jalapeno,ToppingPortion.LeftHalf) .build();
As you can see, such code (if it works) allows us to assemble a pizza one topping at a time. Each line above makes it clear which topping is being added to the pizza (the code is styled so that calls are one per line, to make it look like a to-do list). This increased readability also reduces the possibility of making errors.
Carefully look at this example, and add methods to the
AlaCartePizzaBuilder classes so that this code compiles. Remember:
methods that are identical should be in the abstract class, while methods that
are unique should be in the sub-classes. Furthermore, the
should throw an
IllegalStateException if one attempts to build a pizza
without specifying the size.
You should start by pasting the above code snippet in a new test class (in the
default package). It will produce compiling errors. Read the error messages to
determine what you are missing, and modify the code accordingly. Again,
remember that this test is using classes from the
the original code should remain in the
Create better versions of the provided
VeggiePizza classes by following the same methodology as above: create
classes with identical names in the
betterpizza package, write example
code to create a cheese and veggie pizza similar to above, and modify your code
so that your example works. You must create the counterpart
VeggiePizzaBuilder classes similar to
above. For example, here is how one could create a large, thin crust cheese
ObservablePizza cheese = new CheesePizza.CheesePizzaBuilder() .crust(Crust.Thin) .size(Size.Large) .build();
Observe in the above code snippet that the builder should start with the proper default configuration for the pizza (e.g. you should not have to use the builder to explicitly add cheese or sauce to a cheese pizza).
Add examples to your tests for a medium, stuffed crust pizza and a large, thin crust veggie pizza. Write the test that is the counterpart of the one provided in the starter code to verify the costs of all your pizzas (they should cost exactly the same!)
Discuss these questions with the person next to you, and report to the course staff.
How would you add a new pre-configured pizza in both designs: pepperoni pizza? Which design allows you to add this more conveniently?
Beyond ensuring that the pizzas do not have to mutate, what are some other advantages of using the builder pattern in this specific problem?
Has using the builder pattern this way made some things more complicated? What are they, and could you have implemented things differently to improve?
What if you wanted a cheese pizza, but with white sauce instead? Or a veggie
pizza but without tomatoes? Such “small customizations” from pre-defined
pizzas are common. However in our design, doing this would be inconvenient or
more error-prone. In the current design the only way to achieve this is to use
an alacarte pizza, which foregoes the advantages of starting with a
pre-configured pizza. It would be nice if each type of pizza builder offered
customized methods to modify its toppings. For example, the
CheesePizzaBuilder can offer a method called
will put cheese only on the left half of the pizza instead of the whole pizza.
Add the following public methods to the
rightHalfCheese(). Now try
to construct a medium cheese pizza with a thin crust, that has cheese only in
its left half. What happens? Think about why this is happening before you
As you may have discovered, the problem is that these methods are defined
exclusively in the
CheesePizzaBuilder class. However the
AlaCartePizzaBuilder objects, not
CheesePizzaBuilder objects. Therefore these new methods cannot be called
on the objects returned by these inherited methods!
We have encountered a possible limitation in our design: the builder works correctly in a class hierarchy so long as all builders offer exactly the same methods. Let us enhance our design so that builders can offer customized methods depending on what they are building.
AlaCartePizzaBuilderclass, write a protected method
AlaCartePizzaBuilder returnBuilder()that returns
this. Use this method in each of its methods that modify the pizza, instead of directly returning
Write the same method in the
CheesePizzaBuilderclass, but with a return type of
CheesePizzaBuilder. Use it in the same way in the
CheesePizzaBuilderclass. Note that these methods are identical, except for their return types. If only we can capture this commonality while retaining their ability to return the type of builder they were implemented in...
returnBuildermethod into the
PizzaBuilderclass, by changing the return type to a generic
T. Make this method
abstract. Make the
PizzaBuilderitself generic on
Tby changing its declaration to
public abstract class PizzaBuilder<T>.
Tbe? For it to represent both
CheesePizzaBuilderit should be...something that extends the
PizzaBuilder! This leads to the following doozy of a syntax:
public abstract class PizzaBuilder<T extends PizzaBuilder<T>>.
Java allows us to place restrictions on a generic parameter this way (for example, we have restricted T to be only PizzaBuilder-type things). Make sure you understand exactly what this syntax means before proceeding. It is simpler to understand it if you think about why it came to be this way.
(Note: For situations like this, where we need to use a type parameter to indicate “the current class, itself”, I often like to name the type parameter
Self, or by the initials of the current class:
PizzaBuilder<Self extends PizzaBuilder<Self>>, or
PizzaBuilder<PB extends PizzaBuilder<PB>>, and I leave a Javadoc comment @param PB the specific type of PizzaBuilder being defined, to indicate to implementors how to properly use this type parameter.)
Change your concrete pizza builder classes to properly define the generic parameter. For example, it should be
public static class AlaCartePizzaBuilder extends PizzaBuilder<AlaCartePizzaBuilder>.
Implement the abstract method
returnBuilderin each of your builder classes. This should only involve minor changes to the protected methods you wrote in the first two steps.
Your example of constructing a medium cheese pizza with a thin crust and cheese only in its left half, should now succeed. Write tests for such a pizza.
Explain to the person next to you how and why your code works (or seek their help for why it does not). Similarly, listen to their explanation. Finally explain this to the TA.
Use the same design as above to enhance the builder for the veggie pizza. In addition to the methods offered by all builders, this builder should offer extra methods to remove each topping, sauce or cheese from this pizza. Add tests for this pizza.
Create a “3-topping pizza”, that allows you to create a pizza that has at most 3 toppings on a pizza. The user should be able to remove a topping (e.g. to replace with another topping).