Reading Time: 6 minutesThe Builder pattern eliminates building the complexity of the target object as well as eases the construction by providing such methods that will assemble the object same as building a set of blocks.
By implementing the Builder pattern, there won’t be any need of larger constructors, parameter objects or custom types. Furthermore, the Builder pattern is listed as a Creational Pattern. In JDK this pattern can be seen in java.lang.StringBuilder‘s append method.
In real life examples, the pattern consists of;
- Client: is the endpoint that makes a request to the product,
- Product: The actual complex object itself which is assembled by the concrete builders,
- Director: Has only one responsibility to make a call to the Builder itself,
- Builder: Hold the common methods for Concrete Builders of how the product will be shaped,
- Concrete Builder: They build creates a specific Product, provides ways to decorate the Product as well as constructs the product and returns it.
Advantages:
-Removing multiple and complex constructor creations,
-Providing a method that checks whether the required fields are filled,
-Providing a mechanism to check whether given parameters meet the requirements
-Immutability, this is mostly the developers are looking forward to striving and benefiting it
Disadvantages:
-For each specific Product, there must be a concrete builder created,
For the sake of better apprehending the pattern which is in a way complex, I’ll be giving two examples an Intermediate that gives an overall look to quickly understanding the pattern
and an advanced example which demonstrates all the Roles that are defined above.
Intermediate Example
In this example you will study a simple Person object that will be constructed by a static Person builder class.
The direct construction of the Person object is disallowed by marking the constructor as private. In addition, I have included a simple business logic within the build method that will give you an idea how business logic can be implemented properly.
Last of all, I have created test cases to see the possible behaviors and outcome of the object.
Person Class
package com.tugrulaslan.builderpattern;
import java.util.Date;
public class Person {
/**
* Required Fields
*/
private final String firstName;
private final String lastName;
private final Date dateOfBirth;
/**
* Optional Fields
*/
private final String phoneNumber;
private final String city;
/**
* The private Constructor accepts only the builder which holds the data to initiate the fields in the Person class,
* If optional field are left off null, they are assigned to empty values to prevent NullPointerException
*
* @param personBuilder
*/
private Person(final PersonBuilder personBuilder) {
this.firstName = personBuilder.firstName;
this.lastName = personBuilder.lastName;
this.dateOfBirth = personBuilder.dateOfBirth;
this.phoneNumber = personBuilder.phoneNumber == null ? "" : personBuilder.phoneNumber;
this.city = personBuilder.city == null ? "" : personBuilder.city;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Date getDateOfBirth() {
return dateOfBirth;
}
public String getPhoneNumber() {
return phoneNumber;
}
public String getCity() {
return city;
}
public static class PersonBuilder {
/**
* Required Fields, will be marked as final in the builder class
*/
private final String firstName;
private final String lastName;
private final Date dateOfBirth;
/**
* Optional Fields, will not be marked as final in the builder class
*/
private String phoneNumber;
private String city;
/**
* Constructor of static PersonBuilder that initiates the required fields
*
* @param firstName A String type field, corresponds to the firstName field in the Person object, required,
* @param lastName A String type field, corresponds to the lastName field in the Person object, required ,
* @param dateOfBirth A java.util.Date type field, corresponds to the dateOfBirth field in the Person object, required.
*/
public PersonBuilder(final String firstName, final String lastName, final Date dateOfBirth) {
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth;
}
/**
* @param phoneNumber A String type field, correspond to the phoneNumber field in the Person object, optional.
* @return returns the same PersonBuilder object
*/
public PersonBuilder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
/**
* @param city A String type field, correspond to the city field in the Person object, optional.
* @return returns the same PersonBuilder object
*/
public PersonBuilder city(String city) {
this.city = city;
return this;
}
/**
* Person builder object that has all the required business rules
*
* @return A Person object unless the business rules are met, otherwise exceptions will be thrown
*/
public Person buildPerson() {
Person person = new Person(this);
if (person.firstName == null || person.firstName.length() == 0) {
throw new IllegalArgumentException("First Name cannot be null or empty");
} else if (person.lastName == null || person.lastName.length() == 0) {
throw new IllegalArgumentException("Last Name cannot be null or empty");
} else if (person.dateOfBirth == null) {
throw new IllegalArgumentException("Birth date cannot be null");
} else {
return person;
}
}
}
}
Person Class Test Case
package com.tugrulaslan.test;
import com.tugrulaslan.builderpattern.Person;
import org.junit.Test;
import java.util.Date;
import static org.junit.Assert.assertEquals;
public class PersonTest {
private final static String FIRST_NAME = "Tugrul";
private final static String LAST_NAME = "Aslan";
private final static Date DATE_OF_BIRTH = new java.util.Date();
private final static String PHONE_NUMBER = "+48754345325";
private final static String CITY = "Wrocław";
@Test(expected = java.lang.IllegalArgumentException.class)
public void testRequiredFieldInvalidValue() {
Person person = new Person.PersonBuilder(null, LAST_NAME, null).buildPerson();
}
@Test
public void testProperValuesOptionalValuesEmpty() {
Person person = new Person.PersonBuilder(FIRST_NAME, LAST_NAME, DATE_OF_BIRTH).buildPerson();
assertEquals(person.getFirstName(), FIRST_NAME);
assertEquals(person.getLastName(), LAST_NAME);
assertEquals(person.getDateOfBirth(), DATE_OF_BIRTH);
assertEquals(person.getPhoneNumber(), "");
assertEquals(person.getCity(), "");
}
@Test
public void testProperValuesOptionalValuesFilledIn() {
Person person = new Person.PersonBuilder(FIRST_NAME, LAST_NAME, DATE_OF_BIRTH)
.phoneNumber(PHONE_NUMBER)
.city(CITY)
.buildPerson();
assertEquals(person.getFirstName(), FIRST_NAME);
assertEquals(person.getLastName(), LAST_NAME);
assertEquals(person.getDateOfBirth(), DATE_OF_BIRTH);
assertEquals(person.getPhoneNumber(), PHONE_NUMBER);
assertEquals(person.getCity(), CITY);
}
}
Advanced Example
Advanced example compared to the Intermediate one, is fairly exposes all roles in depth. To better apprehend the topic I have given a kebab example where I’ll walk you through making kebab in a code level from the scratch.
Test cases will prove the outcome and each builder will properly assemble an example of the specific kind of kebab. To redefine each role that is used in this example;
- Client: KebabTest,
- Product: Kebab,
- Director: Chef,
- Builder: KebabBuilder,
- Concrete Builder: AdanaKebab, UrfaKebab and ChickenKebab.
KebabTest
package com.tugrulaslan.test;
import com.tugrulaslan.builderpattern.*;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class KebabTest {
@Test
public void adanaKebabTestCase() {
Chef chef = new Chef();
KebabBuilder adanaKebabBuilder = new AdanaKebab();
chef.setKebabBuilder(adanaKebabBuilder);
chef.makeKebab();
Kebab adanaKebab = chef.getKebab();
assertNotNull(adanaKebab);
assertEquals(adanaKebab.getKebabName(), "Adana Kebab");
assertEquals(adanaKebab.getMeatType(), MeatType.REDMEAT);
assertTrue(adanaKebab.isSpicy());
assertEquals(adanaKebab.getPrice(), 10);
}
@Test
public void urfaKebabTestCase() {
Chef chef = new Chef();
KebabBuilder urfaKebabBuilder = new UrfaKebab();
chef.setKebabBuilder(urfaKebabBuilder);
chef.makeKebab();
Kebab urfaKebab = chef.getKebab();
assertNotNull(urfaKebab);
assertEquals(urfaKebab.getKebabName(), "Urfa Kebab");
assertEquals(urfaKebab.getMeatType(), MeatType.REDMEAT);
assertFalse(urfaKebab.isSpicy());
assertEquals(urfaKebab.getPrice(), 9);
}
@Test
public void chickenKebabTestCase() {
Chef chef = new Chef();
KebabBuilder chickenKebabBuilder = new ChickenKebab();
chef.setKebabBuilder(chickenKebabBuilder);
chef.makeKebab();
Kebab chickenKebab = chef.getKebab();
assertNotNull(chickenKebab);
assertEquals(chickenKebab.getKebabName(), "Chicken Kebab");
assertEquals(chickenKebab.getMeatType(), MeatType.CHICKEN);
assertTrue(chickenKebab.isSpicy());
assertEquals(chickenKebab.getPrice(), 7);
}
}
Kebab
package com.tugrulaslan.builderpattern;
/**
* Kebab class is in the Product Role
*/
public class Kebab {
private String kebabName;
private MeatType meatType;
private boolean spicy;
private int price;
public String getKebabName() {
return kebabName;
}
public void setKebabName(String kebabName) {
this.kebabName = kebabName;
}
public MeatType getMeatType() {
return meatType;
}
public void setMeatType(MeatType meatType) {
this.meatType = meatType;
}
public boolean isSpicy() {
return spicy;
}
public void setSpicy(boolean spicy) {
this.spicy = spicy;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
Chef
package com.tugrulaslan.builderpattern;
/**
* Chef class is in the Director Role
*/
public class Chef {
private KebabBuilder kebabBuilder;
public void setKebabBuilder(KebabBuilder kebabBuilder) {
this.kebabBuilder = kebabBuilder;
}
/**
* @return kebab object that is assembled by the concrete kebab builder class
*/
public Kebab getKebab() {
return kebabBuilder.kebab;
}
/**
* The void method that calls all the available methods in the concrete class
*/
public void makeKebab() {
kebabBuilder.createNewKebab();
kebabBuilder.assignKebabName();
kebabBuilder.assignMeatType();
kebabBuilder.makeSpicy();
kebabBuilder.setKebabPrice();
}
}
KebabBuilder
package com.tugrulaslan.builderpattern;
/**
* KebabBuilder class is in the Builder Role
*/
public abstract class KebabBuilder {
protected Kebab kebab;
/**
* @return current Kebab Object
*/
public Kebab getKebab() {
return kebab;
}
/**
* Returns a new instance of Kebab object
*/
public void createNewKebab() {
kebab = new Kebab();
}
/**
* An abstract template to assign a name to the Kebab
*/
public abstract void assignKebabName();
/**
* An abstract template to assign the Kebab's Meat Type
*/
public abstract void assignMeatType();
/**
* An abstract template to the Kebab spicy or not
*/
public abstract void makeSpicy();
/**
* An abstract template to set Kebab's price
*/
public abstract void setKebabPrice();
}
AdanaKebab
package com.tugrulaslan.builderpattern;
/**
* Adana Kebab concrete class is in the Concrete Builder Role that represents its specifications.
*/
public class AdanaKebab extends KebabBuilder {
@Override
public void assignKebabName() {
kebab.setKebabName("Adana Kebab");
}
@Override
public void assignMeatType() {
kebab.setMeatType(MeatType.REDMEAT);
}
@Override
public void makeSpicy() {
kebab.setSpicy(true);
}
@Override
public void setKebabPrice() {
kebab.setPrice(10);
}
}
UrfaKebab
package com.tugrulaslan.builderpattern;
/**
* Urfa Kebab concrete class is in the Concrete Builder Role that represents its specifications.
*/
public class UrfaKebab extends KebabBuilder {
@Override
public void assignKebabName() {
kebab.setKebabName("Urfa Kebab");
}
@Override
public void assignMeatType() {
kebab.setMeatType(MeatType.REDMEAT);
}
@Override
public void makeSpicy() {
kebab.setSpicy(false);
}
@Override
public void setKebabPrice() {
kebab.setPrice(9);
}
}
ChickenKebab
package com.tugrulaslan.builderpattern;
/**
* Chicken Kebab concrete class is in the Concrete Builder Role that represents its specifications.
*/
public class ChickenKebab extends KebabBuilder{
@Override
public void assignKebabName() {
kebab.setKebabName("Chicken Kebab");
}
@Override
public void assignMeatType() {
kebab.setMeatType(MeatType.CHICKEN);
}
@Override
public void makeSpicy() {
kebab.setSpicy(true);
}
@Override
public void setKebabPrice() {
kebab.setPrice(7);
}
}