27
Design Patterns: Builder
Welcome back to our series on design patterns.
This week are tackling the Builder pattern.
It's another creational design pattern but is a bit different from the other pattern's we've discussed so far in this series.
Today you will learn:
- Understand the core concepts of the Builder pattern.
- Recognize opportunities to use the Builder pattern.
- Understand the pros and cons of the Builder pattern.
“Builder pattern is more verbose than the telescoping constructor pattern, so it should be used only if there are enough parameters, say, four or more.” ― Joshua Bloch, Effective Java
If we google the builder pattern we will get something like this:
It's not wrong, but why does this have to sound so complicated.
In a nutshell:
The Builder pattern is a creational design pattern that allows us:
- Construct complex objects step by step
- Produce different types and representations of an object using the same construction code
Let's imagine that you work for a construction company that builds houses. There are many different types of houses, with all sorts of variation.
For example:
- House
- House with swimming pool
- House with garden
- House with garage
How would we represent this in code?
One way would be to create a parent class called House and then create a bunch of subclasses for each variant.
This approach is bad because, what if we needed a house with a garage and a swimming pool?
We will be forced to create a new class called HouseWithGarageAndSwimmingPool
For every new variant, we are gonna have to create a new subclass, potentially duplicating code and making everything more complex and harder to maintain.
Another approach would be to simply create a constructor that accepts a bunch of parameters that determines it's properties.
It would look something like this:
class House {
constructor(hasSwimmingPool, hasGarage, hasGarden){
// some stuff
}
}
You might be already thinking that this approach isn't very scalable. If we wanted to add more properties we just have to add a new parameter to our constructor. This may leave us with a huge constructor, with complex logic making it hard to maintain.
But what's the solution?
This is where the builder pattern shines.
First of all let's think for a second.
The construction of a house can be broken down into steps?
Yea, we can build the floor first, then the walls, then the roof, etc...
How about we put this into a separate builder class called HouseBuilder
.
interface Builder {
public void buildWalls()
public void buildDoors()
public void buildWindows()
public void buildRoof()
public void buildGarage()
public void buildGarden()
public void buildSwimmingPool()
}
class HouseBuilder implements Builder {
public void buildWalls() {
// build walls
}
public void buildDoors() {
// build doors
}
public void buildWindows() {
// build windows
}
public void buildRoof() {
// build roof
}
public void buildGarage() {
// build garage
}
public void buildGarden() {
// build garden
}
public void buildSwimmingPool(){
// build swimming pool
}
public House getResult(){
// return the fully configured house
}
}
You might be thinking that there's just way too much code, and it's a huge hassle. But you didn't see the added benefit, with this we can create different kinds of house builders for example if we want a house made only with stone, or a house made only using clay. We can simply create two new house builder classes called HouseStoneBuilder
and HouseClayBuilder
. Plus, because we are using the same interface for each builder, the client code can easily swap between builders.
Let's demonstrate an example client:
public House buildHouse(type){
builder = new HouseBuilder()
// build what's common in all houses
builder.buildWalls()
builder.buildDoors()
builder.buildWindows()
builder.buildRoof()
if(type === "withGarage"){
builder.buildGarage()
}
if(type === "withGarden"){
builder.buildGarden()
}
if(type == "withSwimmingPool"){
builder.buildSwimmingPool()
}
House house = builder.getResult()
return house
}
This is certainly much better than we had before, but we can make this even better. The client code doesn't have to know the steps in making these houses, so let us extract the building process to another class called the Director
.
The Director
class is like an engineer, it knows the steps in creating the houses, and uses implementations of the builders to create them.
class Director extends {
Builder builder;
constructor(Builder builder){
this.builder = builder;
}
buildHouse(){
this.builder.buildWalls()
this.builder.buildDoors()
this.builder.buildWindows()
this.builder.buildRoof()
return this.builder.getResult()
}
buildHouseWithSwimmingPool(){
this.builder.buildWalls()
this.builder.buildDoors()
this.builder.buildWindows()
this.builder.buildRoof()
this.builder.buildSwimmingPool()
return this.builder.getResult()
}
// other configurations
}
Now that our Director
class is ready, we can refactor our original client code:
public House buildHouse(type){
builder = new HouseBuilder()
director = new Director(builder)
House house = director.buildHouse()
// you can add if statements for other configs
if(type === "withGarage"){
house = director.buildHouseWithGarage()
}
return house
}
Congratulations, you have just implemented your first builder pattern. Now it's much easier to new types of houses, as well as different configurations.
Knowing the Build pattern without using it, won't do you any good. You have to recognize opportunities where you can apply this pattern.
-
Use the Builder pattern to get rid of huge constructors.
As we have seen in our first example, object creation can be a complex thing. So if you see that you have a huge constructor that takes way too many parameters and is simply complex and hard to maintain. This may be an opportunity to refactor it to the Build pattern.
-
Use the Builder pattern when you want to create different variations of objects.
The Builder pattern allows us to destruct object creation into a step by step process, where you the developer can control each step. This allows us to create different variations of objects using different builder classes and directors.
-
Use the Builder pattern to construct complex objects such as Composite Trees.
As stated above, we control the whole process in object creation, we can even recursively create objects to construct composite trees or other complex objects.
- Single Responsibility Principle: You separate the complex construction code from the product/object.
- Constructing objects step-by-step: This helps us control the whole object creation flow, which allows us to create complex objects.
- Don't Repeat Yourself: We remove the need to duplicate code between objects, which makes the code more maintainable.
- Complexity: Because of the addition of many new classes and interfaces. This increases code complexity.
The Builder pattern can be useful in many real-world scenarios. I hope you got the gist of it, if not then feel free to leave your questions down in the comments.
If you enjoyed this article, share it with your friends and colleagues!
Thanks for reading!
If you want to learn more about the design patterns, I would recommend Diving into Design Patterns. It explains all 23 design patterns found in the GoF book, in a fun and engaging manner.
Another book that I recommend is Heads First Design Patterns: A Brain-Friendly Guide, which has fun and easy-to-read explanations.
27