Introduction
In this series of learning different design patterns, now we came across a new pattern, i.e. the builder design pattern. As discussed in the last article, we saw what a design pattern is, and what it provides when implemented with the factory design pattern.
A design pattern provides a simple solution to common problems that are faced in day-to-day life by software developers. Three basic types of design patterns exist, which are listed below:
- Creational Pattern
- Structural Pattern
- Behavioral Pattern
Along with these, 23 design patterns are classified above in three patterns.
Thus, the pattern described here is like a blueprint that you can customize to solve a particular design problem and they only differ by complexity and scalability of the code.
In this article, we will learn about the builder design pattern. Let’s get started.
Why the Builder Design Pattern?
To start with what is a builder design pattern, one should know why we need this design pattern and what’s the drawback behind the factory pattern to introduce a new design pattern.
In simple language, when we have too many arguments to send in the constructor and is difficult to maintain the order of the values to be set or when we don’t want to send all parameters in object initialization i.e. parametric value as NULL.
Inserting too many attribute values through the constructor and maintaining the order of values respectively is a difficult task. To overcome the issue of ordering and insertion of the values, the builder design pattern has evolved.
Let’s see this example
package builderDesign;
public class CsharpCorner {
private String name;
private String role;
private int reputationPoints;
private int read;
private int profileLikes;
public CsharpCorner(String name, String role, int reputationPoints, int read, int profileLikes) {
super();
this.name = name;
this.role = role;
this.reputationPoints = reputationPoints;
this.read = read;
this.profileLikes = profileLikes;
}
}
Now, what if we introduce new attributes in the class? One way is to create more constructors and another is to introduce setter methods and lose the immutability. By choosing either of both options, you lose something.
Here, the builder pattern will help in consuming additional attributes while retaining immutability. Thus, the builder pattern provides more control over the object creation problem.
Note. The public constructor is to include only required parameters, there's no need to add optional parameters in the constructor.
Builder Design Pattern
Builder Design Pattern is a part of a creational design pattern i.e. it deals with the creation of an object and is an alternative approach for constructing complex objects.
The builder design pattern is used when want to construct different immutable objects using the same object. It provides more control over the object creation process before the learning builder pattern lets us understand why we need this design pattern.
Demo Implementation
Let’s have a class Phone with 5 variables and we are also using a parameterized constructor to set the values. Along with this, we have a toString() method. When we print the object we get that method.
package BuilderDesign;
public class Phone {
private String os;
private String processor;
private int battery;
private double screensize;
private String company;
public Phone(String os, String processor, int battery, double screensize, String company) {
super();
this.os = os;
this.processor = processor;
this.battery = battery;
this.screensize = screensize;
this.company = company;
}
@Override
public String toString() {
return "Phone [os=" + os + ", processor=" + processor + ", battery=" + battery + ", screensize=" + screensize + ", company=" + company + "]";
}
}
Now we will create the PhoneBuilder class, which is responsible for creating a phone. Here, we have all member variables with their setter function. Instead of using “void” in the setter function, we will use the “PhoneBuilder” object that means for any method we set the value you will get the object of PhoneBuilder.
package BuilderDesign;
/*
* RULES
* 1-> Create class and initialize variables
* 2-> Create Setter functions of that variables with datatype of class name (here PhoneBuilder) and return this.
* 3-> Create method similar to "toString" (here getPhone).
* 4-> Create a class (here Phone) like in Encapsulation (with constructor(parameterized) and toString() method).
* 5-> Create a class used for testing the code (here Shop) and add details you want.
*/
public class PhoneBuilder {
private String os;
private String processor;
private int battery;
private double screensize;
private String company;
public PhoneBuilder setOs(String os) {
this.os = os;
return this;
}
public PhoneBuilder setProcessor(String processor) {
this.processor = processor;
return this;
}
public PhoneBuilder setBattery(int battery) {
this.battery = battery;
return this;
}
public PhoneBuilder setScreensize(double screensize) {
this.screensize = screensize;
return this;
}
public PhoneBuilder setCompany(String company) {
this.company = company;
return this;
}
public Phone getPhone() {
return new Phone(os, processor, battery, screensize, company);
}
}
Create the class Shop and set the values you want only & others will take the default value accordion to the datatype.
package BuilderDesign;
public class Shop {
public static void main(String[] args) {
Phone p1 = new PhoneBuilder()
.setCompany("Samsung")
.setBattery(6000)
.setOs("Android10")
.getPhone();
System.out.println(p1);
Phone p2 = new PhoneBuilder()
.setCompany("Htc")
.setOs("Android10")
.setScreensize(15.6)
.getPhone();
System.out.println(p2);
Phone p3 = new PhoneBuilder()
.setBattery(7000)
.setCompany("Samsung")
.setProcessor("Intel")
.getPhone();
System.out.println(p3);
}
}
Output
Phone [os=Android10, processor=null, battery=6000, screensize=0.0, company=Samsung]
Phone [os=Android10, processor=null, battery=0, screensize=15.6, company=Htc]
Phone [os=null, processor=Intel, battery=7000, screensize=0.0, company=Samsung]
From this above demo example, you will have an idea i.e. what the builder pattern is and what is the usage with the option to add only the required values and other values are default NULL.
Realtime case problem Implementation
Here is the real-time problem of any restaurant to calculate the cost of your meal. We have a few options, mentioned below.
- Interface: Item, Packing
- Abstract class: Burger, ColdDrink
- Burger: VegBurger, ChickenBurger
- Cold drink: Pepsi, Coke
- Packing class: Bottle, Wrapper, and other classes.
First, we have two interfaces, Item, and Packing.
package exampleBuilder;
public interface Item {
String name();
Packing packing();
float price();
}
package exampleBuilder;
public interface Packing {
String pack();
}
Then we have two abstract classes for types of food items that implement the interface item.
package exampleBuilder;
public abstract class Burger implements Item {
@Override
public Packing packing() {
return new Wrapper();
}
@Override
public abstract float price();
}
package exampleBuilder;
public abstract class ColdDrink implements Item {
@Override
public Packing packing() {
return new Bottle();
}
@Override
public abstract float price();
}
For home delivery of food items, we need to wrap the food. For that purpose, we need a Wrapper class for Burger and Bottle class for Colddrink, and they are defined as,
package exampleBuilder
public class Bottle implements Packing {
@Override
public String pack() {
return "Bottle";
}
}
package exampleBuilder;
public class Wrapper implements Packing {
@Override
public String pack() {
return "Wrapper";
}
}
Then, a Burger class is divided into two different types of burger Veg and Non-Veg, which extends an abstract class, Burger.
package exampleBuilder;
public class VegBurger extends Burger {
@Override
public String name() {
return "VegBurger";
}
@Override
public float price() {
return 50.25f;
}
}
package exampleBuilder;
public class ChickenBurger extends Burger {
@Override
public String name() {
return "ChickenBurger";
}
@Override
public float price() {
return 80.50f;
}
}
Similarly, the class Colddrink is further divided into two different classes which extends an abstract class, ColdDrink.
package exampleBuilder;
public class Coke extends ColdDrink {
@Override
public String name() {
return "Coke";
}
@Override
public float price() {
return 40.0f;
}
}
package exampleBuilder;
public class Pepsi extends ColdDrink {
@Override
public String name() {
return "Pepsi";
}
@Override
public float price() {
return 35.0f;
}
}
After all these food items are known, we have to get a meal, shown by.
package exampleBuilder;
import java.util.ArrayList;
import java.util.List;
public class Meal {
List<Item> list = new ArrayList<Item>();
public void addItem(Item item) {
list.add(item);
}
public float getCost() {
float cost = 0.0f;
for (Item item : list) {
cost += item.price();
}
return cost;
}
public void showMeal() {
for (Item i : list) {
System.out.print("Item Name " + i.name());
System.out.print("\t Item Packing " + i.packing().pack());
System.out.println("\t Item Price " + i.price());
}
}
}
After the meal is ready, we need a MealBuilder class similar to a waiter taking your food order whether veg or non-veg meal. This is shown by.
package exampleBuilder;
public class MealBuilder {
public Meal vegMeal() {
Meal meal = new Meal();
meal.addItem(new VegBurger());
meal.addItem(new Coke());
return meal;
}
public Meal nonvegMeal() {
Meal meal = new Meal();
meal.addItem(new ChickenBurger());
meal.addItem(new Pepsi());
return meal;
}
}
At last, we just place our order and wait for the delicious food. Here the process is the same, except for the service time that the restaurant workers need to make your food ready. This is neglected here.
package exampleBuilder;
public class PlaceOrder {
public static void main(String[] args) {
MealBuilder mealBuilder = new MealBuilder();
Meal veg = mealBuilder.vegMeal();
veg.showMeal();
System.out.println("Meal Cost: " + veg.getCost());
Meal nonVeg = mealBuilder.nonvegMeal();
nonVeg.showMeal();
System.out.println("Meal Cost: " + nonVeg.getCost());
}
}
Output
Item Name VegBurger Item Packing Wrapper Item Price 50.25
Item Name Coke Item Packing Bottle Item Price 40.0
Meal Cost: 90.25
Item Name ChickenBurger Item Packing Wrapper Item Price 80.5
Item Name Pepsi Item Packing Bottle Item Price 35.0
Meal Cost: 115.5
Note. We don’t use any setter method, so its state can not be changed once it has been built thus providing the desired immutability. Alternatively, if we have a setter method one can change the value from outside.
Existing Implementations in JDK
- StringBuffer
- StringBuilder, and
- ByteBuffer
Advantages
- Flexible Design.
- Readable code(easily).
- Reduces the number of parameters in the constructor thus no need to pass NULL for optional parameters.
- An object is instantiated in a complete state.
- Build immutable objects.
Disadvantage
A drawback with the builder design pattern that I came to know is that it increases the lines of code.
Summary
Thus, coming towards the end of this article, we learned how to create a builder design pattern.
What did we learn?
- What is the design pattern?
- Why the builder design pattern?
- Builder design pattern.
- Demo implementation.
- Realtime case implementation.
- Existing implementations in JDK.
- Advantages & disadvantages of a builder design pattern.