Table of Contents
Explanation
The Builder design pattern is a creational pattern that helps you to build complex objects. Where the Template Method pattern focuses more on customizing steps of an algorithm, the Builder pattern focuses on object creation. The complex object that you want to create is composed out of several other simpler objects. Typically, a director class directs the construction of the complex object.
Use cases:
- You want to maintain a clear overview when creating a complex object or objects.
- You want more control over your object creation process.
- You can exchange components of the main object that you want to build to get variants of the object.
- You want to keep your class constructors short.
Pitfalls:
- Keeping the separation of concerns for different classes, like the Director class and component classes.
- The temptation to create more and more builders or nested builders which make the object creation complex to follow and maintain.
- Over complicating a use case that can be solved simpler/better in another way.
Java Example
Let’s create a hamburger builder. You can read the code from top to bottom and hopefully the comments make sense enough to understand what’s going on.
package com.chosengambit.designpatterns;
import com.chosengambit.builder.Director;
import com.chosengambit.builder.Hamburger;
import com.chosengambit.builder.HamburgerBuilder;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// the director class is responsible for assembling a new hamburger
Director hDirector = new Director();
try {
// you can create a default product or assemble a custom one
Hamburger hamburger = hDirector.CreateDefaultBurger();
System.out.println(hamburger.toString());
// custom burger
HamburgerBuilder hamburgerBuilder = new HamburgerBuilder();
hamburgerBuilder.setBread(new HamburgerBuilder.Ciabatta());
hamburgerBuilder.addProtein(new HamburgerBuilder.Vegan());
// triple portion of red onion
hamburgerBuilder.addOnion(new HamburgerBuilder.RedOnion());
hamburgerBuilder.addOnion(new HamburgerBuilder.RedOnion());
hamburgerBuilder.addOnion(new HamburgerBuilder.RedOnion());
hamburger = hDirector.CreateCustomBurger(hamburgerBuilder);
System.out.println(hamburger.toString());
}
catch (Exception e) {
System.out.println(e.toString());
}
}
}
Javapackage com.chosengambit.builder;
public class Director {
/**
* Returns a custom made burger
* @param hamburgerBuilder
* @return Hamburger
* @throws com.chosengambit.builder.HamburgerBuilder.ProductIncompleteException
*/
public Hamburger CreateCustomBurger(HamburgerBuilder hamburgerBuilder) throws HamburgerBuilder.ProductIncompleteException {
return hamburgerBuilder.getProduct();
}
/**
* Return the default burger of the restaurant
* @return Hamburger
* @throws com.chosengambit.builder.HamburgerBuilder.ProductIncompleteException
*/
public Hamburger CreateDefaultBurger() throws HamburgerBuilder.ProductIncompleteException {
HamburgerBuilder hamburgerBuilder = new HamburgerBuilder();
hamburgerBuilder.setBread(new HamburgerBuilder.Bun())
.addLettuce(new HamburgerBuilder.Iceberg())
.addOnion(new HamburgerBuilder.YellowOnion())
.addProtein(new HamburgerBuilder.NonVegan());
// add sauce
hamburgerBuilder.addSauce(new HamburgerBuilder.Ketchup());
return hamburgerBuilder.getProduct();
}
}
Javapackage com.chosengambit.builder;
import java.util.ArrayList;
import java.util.List;
/**
* Builder class that can build Hamburgers
* @author ChosenGambit
*/
public class HamburgerBuilder {
// generalized product parts
public interface IBread { }
public interface ILettuce { }
public interface IProtein { }
public interface IOnion { }
public interface ISauce { }
// quick implementations of product parts
public static class CornBread implements IBread {}
public static class Bun implements IBread {}
public static class Ciabatta implements IBread {}
public static class Rucola implements ILettuce {}
public static class Iceberg implements ILettuce {}
public static class Vegan implements IProtein {}
public static class NonVegan implements IProtein {}
public static class RedOnion implements IOnion {}
public static class YellowOnion implements IOnion {}
public static class Ketchup implements ISauce {}
public static class Mayonaise implements ISauce {}
public class ProductIncompleteException extends Exception {
public ProductIncompleteException(String message) {
super(message);
}
}
private Hamburger hamburger;
public HamburgerBuilder() {
this.hamburger = new Hamburger();
}
// because "this" object is returned, you can chain the creation of hamburgers
// I use "set" as naming convention when only one item of this kind can be added
// we can only have one bread
public HamburgerBuilder setBread(IBread bread) {
this.hamburger.setBread(bread);
return this;
}
// and "add" when multiple items can be added
public HamburgerBuilder addLettuce(ILettuce lettuce) {
this.hamburger.getLettuceList().add(lettuce);
return this;
}
public HamburgerBuilder addProtein(IProtein protein) {
this.hamburger.getProteinList().add(protein);
return this;
}
public HamburgerBuilder addOnion(IOnion onion) {
this.hamburger.getOnionList().add(onion);
return this;
}
public HamburgerBuilder addSauce(ISauce sauce) {
this.hamburger.getSauceList().add(sauce);
return this;
}
public void reset() {
this.hamburger = new Hamburger();
}
public Hamburger getProduct() throws ProductIncompleteException {
if (this.hamburger.getBread() == null) {
throw new ProductIncompleteException("No bread has been set, which is mandatory.");
}
if (this.hamburger.getProteinList().size() == 0) {
throw new ProductIncompleteException("Set at least one kind of protein.");
}
return this.hamburger;
}
}
Javapackage com.chosengambit.builder;
import java.util.ArrayList;
import java.util.List;
// basically a list with ingredients that make the hamburger
// mostly getters and setters
public class Hamburger {
// the ingredients
private HamburgerBuilder.IBread bread;
private List<HamburgerBuilder.ILettuce> lettuceList = new ArrayList<HamburgerBuilder.ILettuce>();
private List<HamburgerBuilder.IProtein> proteinList = new ArrayList<HamburgerBuilder.IProtein>();
private List<HamburgerBuilder.IOnion> onionList = new ArrayList<HamburgerBuilder.IOnion>();
private List<HamburgerBuilder.ISauce> sauceList = new ArrayList<HamburgerBuilder.ISauce>();
public HamburgerBuilder.IBread getBread() {
return bread;
}
public void setBread(HamburgerBuilder.IBread bread) {
this.bread = bread;
}
public List<HamburgerBuilder.ILettuce> getLettuceList() {
return lettuceList;
}
public void setLettuceList(List<HamburgerBuilder.ILettuce> lettuceList) {
this.lettuceList = lettuceList;
}
public List<HamburgerBuilder.IProtein> getProteinList() {
return proteinList;
}
public void setProteinList(List<HamburgerBuilder.IProtein> proteinList) {
this.proteinList = proteinList;
}
public List<HamburgerBuilder.IOnion> getOnionList() {
return onionList;
}
public void setOnionList(List<HamburgerBuilder.IOnion> onionList) {
this.onionList = onionList;
}
public List<HamburgerBuilder.ISauce> getSauceList() {
return sauceList;
}
public void setSauceList(List<HamburgerBuilder.ISauce> sauceList) {
this.sauceList = sauceList;
}
@Override
public String toString() {
StringBuilder burgerString = new StringBuilder("Burger is assembled out of: ");
if (bread != null) burgerString.append(bread.getClass().getSimpleName());
proteinList.forEach(item -> burgerString.append(" "+item.getClass().getSimpleName()));
lettuceList.forEach(item -> burgerString.append(" "+item.getClass().getSimpleName()));
onionList.forEach(item -> burgerString.append(" "+item.getClass().getSimpleName()));
sauceList.forEach(item -> burgerString.append(" "+item.getClass().getSimpleName()));
return burgerString.toString();
}
}
JavaOutput:
Burger is assembled out of: Bun NonVegan Iceberg YellowOnion Ketchup
Burger is assembled out of: Ciabatta Vegan Vegan RedOnion RedOnion RedOnion
Python Example
class Bread():
pass
class CornBread(Bread):
pass
class Bun(Bread):
pass
class Ciabatta(Bread):
pass
class Lettuce:
pass
class Rucola(Lettuce):
pass
class Iceberg(Lettuce):
pass
class Protein:
pass
class Vegan(Protein):
pass
class NonVegan(Protein):
pass
class Onion:
pass
class RedOnion(Onion):
pass
class YellowOnion(Onion):
pass
class Sauce:
pass
class Ketchup(Sauce):
pass
class Mayonnaise(Sauce):
pass
# Hamburger class
class Hamburger:
def __init__(self):
self.bread = None
self.lettuce_list = []
self.protein_list = []
self.onion_list = []
self.sauce_list = []
def __str__(self):
return (f"Burger with bread: " + self.c_name(self.bread) + ", lettuce: " + "".join(
[self.c_name(s) for s in self.lettuce_list]) +
" protein: " + "".join([self.c_name(s) for s in self.protein_list]) +
" onion: " + "".join([self.c_name(s) for s in self.onion_list]) +
" sauce: " + "".join([self.c_name(s) for s in self.sauce_list]))
def c_name(self, val):
return str(val.__class__.__name__)
# Builder class
class HamburgerBuilder:
def __init__(self):
self.hamburger = Hamburger()
def set_bread(self, bread):
self.hamburger.bread = bread
return self
def add_lettuce(self, lettuce):
self.hamburger.lettuce_list.append(lettuce)
return self
def add_onion(self, onion):
self.hamburger.onion_list.append(onion)
return self
def add_protein(self, protein):
self.hamburger.protein_list.append(protein)
return self
def add_sauce(self, sauce):
self.hamburger.sauce_list.append(sauce)
return self
def get_product(self):
return self.hamburger
# Director constructs a Hamburger
class Director:
def create_custom_burger(self, hamburger_builder):
return hamburger_builder.getProduct()
def create_default_burger(self):
hamburger_builder = HamburgerBuilder()
hamburger_builder.set_bread(Bun()) \
.add_lettuce(Iceberg()) \
.add_onion(YellowOnion()) \
.add_protein(NonVegan()) \
.add_sauce(Ketchup())
return hamburger_builder.get_product()
def main():
director = Director()
try:
hamburger = director.create_default_burger()
print(hamburger)
except Exception as e:
print(str(e))
if __name__ == "__main__":
main()
PythonOutput:
Burger with bread: Bun, lettuce: Iceberg protein: NonVegan onion: YellowOnion sauce: Ketchup
Conclusion
With the builder design pattern, you are able to construct complex objects which are made out of other objects. Furthermore, the add/set functions/methods in the HamburgerBuilder class all return its own instance which makes chaining the build process possible.
References
Freeman, E., Bates, B., & Sierra, K. (2004). Head first design patterns.
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.
Wikipedia contributors. (2024, 10 september). Software design pattern. Wikipedia. https://en.wikipedia.org/wiki/Software_design_pattern