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....
//...
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.
Builder pattern is a solution to these problems when creating a complex product. It will separate 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 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.
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 the Cook.
Builder (Cook): This is an 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), it's possible for products to 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 (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 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();
}
Note: We can create additional classes like HeadChef, such as PrimeChef, which may have a different construction process depending on the needs of the company. Then, we can use the PrimeChef class with existing Cooks to get new, desirable pizzas! Alternatively, we can also define a new construction process within the HeadChef class itself.
In our example, we only considered a single construction process, but there can be many processes with slight differences. For example, perhaps a super-fast variant of pizza has some additional steps compared to a regular pizza.
public class HeadChef {
private Cook cook;
public HeadChef(Cook cook) {
this.cook = cook;
}
public void makePizza() {
//...
}
public void makeSuperFastPizza() {
//...
}
}
We can use the Builder pattern when:
In our example of making pizzas, the process of creation is defined by the HeadChef class, while the parts that make up the object are implemented in the concrete builders (ItalianCook and MexicanCook).
Notice that the Builder class (Cook) hides the internal representation of the Pizza product i.e. it hides the classes that define the various parts of the pizza and how they are assembled to create the final product. This means the process defined by the HeadChef class is independent of the parts of the pizza product.
It is also important to be able to easily add new representations and remove existing representations of the complex product. The Builder pattern allows for this flexibility.
The Builder pattern is often used in creating objects that have multiple configuration options or parameters. Instead of providing a long list of constructor parameters or multiple overloaded constructors, Builder pattern provides a more flexible and readable way to construct objects.
Here are some popular applications of the Builder pattern:
We observed some possible, but inefficient approaches, such as using telescopic constructors and constructing objects using setter methods in the consumer code. Then, we learned about the Builder pattern and explored how it addresses the issues present in the previous approaches.
For example, it allows for more manageable consumer code and ensures the correct order of the construction process. Additionally, we examined its applicability and its impact on the overall creational process.
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!
Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.
©2023 Code Algorithms Pvt. Ltd.
All rights reserved.