Builder is a creational design pattern that 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 construction process to create different versions of the object.
To understand the Builder design pattern, let's use the example of making pizzas. The process of making a pizza consists of a series of steps: first, we make the dough, and then we add the base, toppings, and sauce. Finally, we bake the pizza.
Creating a complex object like a pizza requires step-by-step initialization of many fields. So, one solution is to define the initialization code inside a constructor with multiple parameters. To implement this, we can define a Pizza class with fields such as dough, base, and toppings, and use the constructor of the Pizza class to initialize these fields.
//Pizza class with constructor
public class Pizza {
private String dough;
private String base;
private String toppings;
private String sauce;
private String bake;
private String cheese;
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:
//set of overloaded constructors which are added in Pizza class
public Pizza(String dough, String base, String toppings,
String sauce, String bake, String cheese)
{
//...
}
public Pizza(String dough, String base, String sauce,
String bake, String cheese)
{
//pizza without toppings...
}
public Pizza(String dough, String base, String toppings,
String sauce, String bake)
{
//pizza without cheese...
}
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.
//...
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....
//...
The Builder pattern is a solution to these problems when creating a complex product. It separates the construction process from the representation of the object, which will help us to use the same process to create different versions of the object.
We know that all pizzas follow the same process, but the implementation 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.
Builder (Cook): Interface that declares steps for constructing a product that is common to all of its subclasses or concrete builders.
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. Note: Concrete builders may produce products that don’t follow the common interface.
Director (HeadChef): This class defines the proper order in which all the construction steps should be invoked, so we can create and reuse specific configurations of products. It uses the Builder interface to create an instance of the complex product.
Product (Pizza): The final objects returned by the concrete builders. While in this example all products belong to the same class (Pizza), products can belong to different class hierarchies or interfaces. 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 builder's objects with the Director. The Director uses this builder object to construct the product. Note: The client can do this just once using the constructor parameters of the director. There can be another approach: the client can pass the builder object to the construction method of the director.
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.
public 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 interface Cook, which includes all the steps involved in construction.
public interface Cook {
public void buildDough();
public void buildBase();
public void buildToppings();
public void buildSauce();
public void buildBake();
public void buildCheese();
public Pizza getPizza();
}
As mentioned above, 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.
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 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 finalPizza = this.pizza;
this.pizza = new Pizza();
return finalPizza;
}
}
MexicanCook Class
public 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 finalPizza = this.pizza;
this.pizza = new Pizza();
return finalPizza;
}
}
In the getPizza() method, we set the pizza field to a new Pizza object so that the same Cook object 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 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). 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();
}
public class HeadChef {
private Cook cook;
public HeadChef(Cook cook) {
this.cook = cook;
}
public void makePizza() {
//...
}
public void makeSuperFastPizza() {
//...
}
}
We can use Builder when:
Thanks to Ankit Nishad for his contribution in creating the first version of this content. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy oops!