Lab 3: The Builder pattern
Objectives
1 Introduction
2 Problem
3 What to do
3.1 Part 1:   A better pizza
3.2 Part 2:   A better Ala-carte pizza
3.3 Part 3:   A better Cheese and Veggie pizza
3.4 Questions to ponder/  discuss
3.5 Part 4:   A pizza that can be customized more easily
3.6 Questions to ponder/  discuss
3.7 Optional Part 5:   A customizable veggie pizza
4 Optional Part 6:   Enhanced builders
8.9

Lab 3: The Builder pattern

Starter files: code.zip

Due: Friday Sept 20 at 08:59PM

Objectives

The objectives of this lab are:

Note: the final design in this lab is inspired from the book Effective Java by Joshua Bloch.

1 Introduction

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 (CheesePizza and 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.

2 Problem

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 addTopping and removeTopping 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.

3 What to do

3.1 Part 1: A better pizza

We first remove the possibility of mutating a pizza.

  1. Create a new package called betterpizza. This package will contain a “better” version of this design.

  2. Create a new interface called ObservablePizza in this package. This interface will contain only those methods from the Pizza interface that do not mutate (cost and hasTopping). Copy their signatures and documentation exactly from the given Pizza interface into the new ObservablePizza interface.

  3. Remove these two methods from the Pizza interface, and make the Pizza interface extend the ObservablePizza interface.

  4. Create a copy of the provided AlaCartePizza class 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.

3.2 Part 2: A better Ala-carte pizza

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 pizza package.

  1. Modify the code so that the (new) AlaCartePizza class implements the ObservablePizza interface from the same package.

  2. Remove all public methods from the (new) AlaCartePizza class that are not present in the ObservablePizza interface.

  3. Create a new abstract class named PizzaBuilder in the betterpizza package. This class represents a pizza builder.

  4. Add a public, static inner class called AlaCartePizzaBuilder to the AlaCartePizza class. This builder class should extend the PizzaBuilder class.

  5. Add a new protected constructor in the (new) AlaCartePizza that 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 PizzaBuilder or 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 build method 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 betterpizza package: the original code should remain in the pizza package.

3.3 Part 3: A better Cheese and Veggie pizza

Create better versions of the provided CheesePizza and VeggiePizza classes by following the same methodology as above: create classes with identical names in the betterpizza package, create the protected constructors that take in size, crust, and toppings, 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 CheesePizzaBuilder and VeggiePizzaBuilder classes similar to above. For example, here is how one could create a large, thin crust cheese pizza:

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!)

3.4 Questions to ponder/discuss

Discuss these questions with the person next to you, and report to the course staff.

  1. How would you add a new pre-configured pizza in both designs: pepperoni pizza? Which design allows you to add this more conveniently?

  2. Beyond ensuring that the pizzas do not have to mutate, what are some other advantages of using the builder pattern in this specific problem?

  3. Has using the builder pattern this way made some things more complicated? What are they, and could you have implemented things differently to improve?

3.5 Part 4: A pizza that can be customized more easily

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 leftHalfCheese which will put cheese only on the left half of the pizza instead of the whole pizza.

Add the following public methods to the CheesePizzaBuilder: noCheese(), leftHalfCheese(), 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 proceed.

As you may have discovered, the problem is that these methods are defined exclusively in the CheesePizzaBuilder class. However the crust() and size() do not return CheesePizzaBuilder objects! Most likely they return AlaCartePizzaBuilder or PizzaBuilder objects, depending on how you remove the duplicate code. 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. The below steps will work regardless of which class holds your deduplicated code!

  1. In the AlaCartePizzaBuilder class, 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 this.

  2. Write the same method in the CheesePizzaBuilder class, but with a return type of CheesePizzaBuilder. Use it in the same way in the CheesePizzaBuilder class. 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...

  3. Abstract the returnBuilder method into the PizzaBuilder class, by changing the return type to a generic T. Make this method abstract. Make the PizzaBuilder itself generic on T by changing its declaration to public abstract class PizzaBuilder<T>.

    What should T be? For it to represent both AlaCartePizzaBuilder and CheesePizzaBuilder it 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.)

  4. At this point, if you have implemented methods in your PizzaBuilder class that would return this, rewrite them so they return returnBuilder instead.

  5. Change your concrete pizza builder classes to properly define the generic parameter. For example, it should be public static class AlaCartePizzaBuilder extends PizzaBuilder<AlaCartePizzaBuilder>.

  6. Implement the abstract method returnBuilder in 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.

3.6 Questions to ponder/discuss

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.

3.7 Optional Part 5: A customizable veggie pizza

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.

4 Optional Part 6: Enhanced builders

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).