BuilderPattern
BuilderPattern
The Builder design pattern helps us create complex objects by providing a step-by-step
construction process. It separates the construction of the object from its representation,
which means we can use the same process to create different versions of the object.
This is a creational design pattern that makes it easy to create complex objects.
Problem Statement
To help understand the Builder design pattern, let's use the example of making pizzas.
A pizza consists of a series of steps: first, we make the dough, then we add the base,
toppings, and sauce. Finally, we bake the pizza. The Builder design pattern allows us to
follow a clear process to create a complex object, like a pizza, by breaking it down into
smaller steps.
Solution Approach 1
We can consider the Pizza class as a representation of a pizza, with fields like dough,
base, and toppings. To create a pizza object, we can use the constructor of the Pizza
class.
public Pizza (String dough, String base, String toppings, String sauce, String bake,
String cheese)
{
this.dough = dough;
this.base = base;
this.toppings = toppings;
this.sauce = sauce;
this.bake = bake;
this.cheese = cheese;
}
//...}
Using the Pizza class constructor can help us create a pizza, but what if customers
have different preferences? For example, one customer might want extra toppings and
mozzarella cheese, while another customer might want no toppings at all. In this case,
using a constructor alone may not be sufficient to handle different customization
requests. So we need to change our Pizza class and add a set of overloaded
constructors like this:
//...
}
public Pizza (String dough, String base, String sauce, String bake, String cheese)
{
public Pizza (String dough, String base, String toppings, String sauce, String bake)
{
Drawbacks
Technically, the set of overloaded constructors will give an error because the compiler
will not be able to distinguish between the second and third constructors due to their
same signature. Therefore, this is a crucial drawback: if the class fields have similar
data types, we will have difficulty using constructor overloading for instantiation.
As the number of combinations of parameters increases, the number of constructors will
increase. So this approach is inefficient for complex and heavy classes.
Takeaway: This approach is often termed a telescopic constructor pattern, an anti-
pattern. So in place of using this pattern, we should try a better approach.
To address the limitations of the telescopic constructor pattern, we can use setter
methods to initialize the fields in a class. This way, we can specify the values for each
field individually, rather than using a single constructor with a large number of
parameters.
//setter methods to initialize the fields in a class
//...
public void setDough (String dough) {
this.dough = dough;
}
public void setBase (String base) {
this.base = base;
}
public void setSauce (String sauce) {
this.sauce = sauce;
}
//and so on....
//...
Drawbacks
To use this approach, consumers have to call the setter methods in the correct order
with the appropriate parameters. For example, if a consumer calls setSauce() before
setDough(), they won't end up with a pizza. This can be a problem if the consumer
doesn't know the proper order of the steps, as it's important to follow the recipe correctly
to make a pizza. Using setter methods requires the consumer to have a good
understanding of the process for creating the object.
Takeaway
It's important to consider the order in which the steps are performed, and we don't want
the consumer code to be responsible for this. Additionally, there may be many types of
pizzas that require each step to be performed differently, which can be difficult to
manage in the consumer code.
The Builder pattern is a solution to these problems when creating a complex product. It
helps to separate the construction process from the representation of the object, which
allows us to use the same process to create different versions of the object.
An efficient solution approach using Builder Pattern
We know that all pizzas follow the same process, but the implementation of the steps
may vary. For example, an Italian pizza has different toppings and cheese from a
Mexican pizza, but the steps to make both pizzas are the same. To handle this, we can
separate the recipe from the process of creating the pizza.
To do this, we can hire a HeadChef who knows the recipe, and specialized Cooks who
can make specific types of pizzas. For example, an ItalianCook knows how to make an
Italian pizza, and a MexicanCook knows how to make a Mexican pizza. The HeadChef's
role is to provide the recipe to the Cook, who follows the recipe and returns the finished
pizza to the HeadChef. This way, the HeadChef can manage the overall process of
making the pizza, while the Cooks handle the specific steps for each type of pizza.
Each Cook is expected to follow the company's policies when making a pizza, and they
are only allowed to use a common set of steps. However, they must implement these
steps according to the requirements of their specific pizza. For example, a
MexicanCook can't add a new component to the Pizza class. To solve this issue, we
can introduce an interface called Cook that declares all the steps involved in making a
pizza.
In terms of classes and interfaces, the HeadChef class defines the steps in the correct
order. The Cook interface consists of methods to set the fields required for the Pizza
product, such as dough and sauce. Subclasses of Cook, like MexicanCook and
ItalianCook, implement the methods provided in the Cook interface. Finally, the
completed Pizza product is returned by one of the concrete subclasses of Cook.
Components and Structure of Builder Pattern
Builder (Cook): This is an interface that declares steps for constructing a product that
are common to all of its subclasses.
Concrete Builders (ItalianCook, MexicanCook): These classes implement the methods
of the Builder interface in different ways to meet the demands of consumers. Different
concrete builders provide different implementations, so we can get different versions of
the complex product.
Director (HeadChef): This class defines the proper order in which all the construction
steps should be invoked. It uses the Builder interface to create an instance of the
complex product.
Product (Pizza): The final objects are returned by the concrete builders. While in this
example all products belong to the same class (Pizza), it's possible for products to
belong to different classes. In that case, the consumer code would call the method of
the Concrete builders directly to get the final product.
Client (Consumer): This part of the code associates one of the concrete builders with
the director, and then constructs the product using the director class.
Implementation Approach (Java)
First, we create the Product class, Pizza, which has various fields and setter methods
for those fields (e.g., setDough(), setBase(), etc). It's important to note that these fields
can be objects of other classes as well.
//required product
public static class Pizza {
private String dough;
private String base;
private String toppings;
private String sauce;
private String bake;
private String cheese;
public void setDough (String dough) {
this.dough = dough;
}
public void setBase (String base) {
this.base = base;
}
public void setToppings (String toppings) {
this.toppings = toppings;
}
public void setSauce (String sauce) {
this.sauce = sauce;
}
public void setBake (String bake) {
this.bake = bake;
}
public void setCheese (String cheese) {
this.cheese = cheese;
}
public void showPizza () {
System.out.println (dough+", "+base+", "+toppings+", "+sauce+", "+bake+",
"+cheese);}}
Next, we create the Builder class, Cook, which declares an interface that includes all the
steps involved in construction.
public static interface Cook {
public void buildDough();
public void buildBase();
public void buildToppings();
public void buildSauce();
public void buildBake();
public void buildCheese();
public Pizza getPizza();
}
It's important to consider whether the Builder interface should include a method for
returning the instance of the Pizza class, or if the concrete subclasses should handle
this on their own. This depends on the final representations of the complex product. If all
the possible final representations belong to the same Pizza class, it makes sense to
include a method in the Builder interface to retrieve the final product.
However, if the final products are very different, it may not be appropriate to represent
them with the same base class or interface. In that case, we would need to handle the
retrieval of the final product individually in the concrete builder classes.
We can then create subclasses of the Cook interface, such as MexicanCook and
ItalianCook, which provide implementations of the construction steps declared in Cook.
These concrete builders will apply all the steps and return the final product.
//ItalianCook Class
public static class ItalianCook implements Cook {
private Pizza pizza;
public ItalianCook() {
this.pizza = new Pizza();
}
@Override
public void buildDough() {
pizza.setDough("Italian Dough");
}
@Override
public void buildBase() {
pizza.setBase("Italian Base");
}
@Override
public void buildToppings() {
pizza.setToppings("Italian Toppings");
}
@Override
public void buildSauce() {
pizza.setSauce("Italian Sauce");
}
@Override
public void buildBake() {
pizza.setBake("Bake");
}
@Override
public void buildCheese() {
pizza.setCheese("Cheese");
}
@Override
public Pizza getPizza() {
Pizza final_pizza = this.pizza;
this.pizza = new Pizza(); return final_pizza; }}
//MexicanCook Class
public static class MexicanCook implements Cook {
private Pizza pizza;
public MexicanCook() {
this.pizza = new Pizza();
}
@Override
public void buildDough() {
pizza.setDough("Mexican Dough");
}
@Override
public void buildBase() {
pizza.setBase("Mexican Base");
}
@Override
public void buildToppings() {
pizza.setToppings("Mexican Toppings");
}
@Override
public void buildSauce() {
pizza.setSauce("Mexican Sauce");
}
@Override
public void buildBake() {
pizza.setBake("Bake");
}
@Override
public void buildCheese() {
pizza.setCheese("Cheese");
}
@Override
public Pizza getPizza() {
Pizza final_pizza = this.pizza;
this.pizza = new Pizza();
return final_pizza;
}
}
In the getPizza() method, we set the pizza field to a new Pizza object so that the same
Cook object (or a concrete subclass of Cook) can be used to make more pizzas in the
future.
Then, we declare the Director class, HeadChef, which is responsible for defining the
order in which the steps are executed. There can be multiple Directors, or a single
Director may have multiple construction processes.
public static class HeadChef {
private Cook cook;
public HeadChef (Cook cook) {
this.cook = cook;
}
public void makePizza() {
cook.buildDough();
cook.buildBase();
cook.buildToppings();
cook.buildSauce();
cook.buildBake();
cook.buildCheese();
}}
Finally, we create the Consumer code, which creates a concrete builder object and
passes it to the Director (HeadChef). The Director then uses this builder object to apply
the construction process, and the complex product is retrieved either through the
Director or directly from the builder.
It's also possible to retrieve the final product from the builder because the Client
typically configures the Director with the appropriate concrete builder. This means the
Client knows which concrete builder will produce the desired product.
public static void main (String[] args){
Cook cook = new ItalianCook();
HeadChef headchef = new HeadChef (cook);
headchef.MakePizza();
Pizza pizza = cook.GetPizza();
pizza.ShowPizza();
cook = new MexicanCook();
headchef = new HeadChef (cook);
headchef.MakePizza();
pizza = cook.GetPizza();
pizza.ShowPizza();
}