Design patterns in life and Ruby — gain an intuitive understanding of OO design patterns by linking them with real-life examples.
The builder pattern is a very commonly used pattern. But its definition can be a bit confusing at first glance.
The builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations.
Fear not. The definition will become much clearer after we build some cars. ? ? ? ?
Let’s start by building a standard car.
class StandardCarBuilder def build car = '' # 1. build car frame car << "This is a standard car\n" # 2. add an engine car << " with an engine\n" # 3. add front wheels car << " with two front wheels\n" # 4. add back wheels car << " with two back wheels\n" # 5. add dashboard car << " with a dashboard\n" # 6. add a energy source car << " with a fuel tank\n" # return the car car end end
To keep our example simple, we will only pay attention to car frames, engines, wheels, dashboard, and energy sources and ignore the rest.
The builder is easy to use.
Let’s also build a sports version of the car.
class SportsCarBuilder def build car = '' # 1. build car frame car << "This is a sport car\n" # 2. add an engine car << " with a powerful engine\n" # 3. add front wheels car << " with two front wheels suitable for mountain paths\n" # 4. add back wheels car << " with two back wheels suitable for mountain paths\n" # 5. add dashboard car << " with a fancy sporty dashboard\n" # 6. add a energy source car << " with a big fuel tank\n" # return the car car end end
The sports version has a more powerful engine, wheels suitable for mountain paths, a fancier dashboard, and a bigger fuel tank.
We can use the SportsCarBuilder
the same way we use StandardCarBuilder
.
We also want to provide an environmentally friendly option: an electronic car.
class ElectronicCarBuilder def build car = '' # 1. build car frame car << "This is an electronic car\n" # 2. add an engine car << " with a electronic engine\n" # 3. add front wheels car << " with two front wheels\n" # 4. add back wheels car << " with two back wheels\n" # 5. add dashboard car << " with a dashboard showing current battery level\n" # 6. add a energy source car << " with a battery\n" # return the car car end end
The electronic version has an electronic engine, standard wheels, a dashboard that shows the current battery level, and a battery.
We can use the ElectronicCarBuilder
the same way we use the above two builders.
Take a Step Back
Take a look at all three car builders. Notice the process they use to construct cars is the same:
- Build a car frame
- Add an engine
- Add front wheels
- Add back wheels
- Add a dashboard
- Add an energy source
- Return the car
The part that varies is the implementation. For example, all three builders implement step 2, “Add an engine”, differently. The StandardCarBuilder
adds a standard engine. The SportsCardBuilder
adds a more powerful engine. The ElectronicCarBuilder
adds an electronic engine.
Time to Refactor Our Code
We can refactor these three builders to follow a design principle we talked about earlier — encapsulate what varies and separate it from the stable part of the code.
The stable part of the code is the process we use to construct a car. The part that varies is the implementation of each step in the process. We want to separate the construction process from the implementation.
Let’s start by reducing each builder to only contain the implementation of each step.
class StandardCarBuilder attr_reader :car def initialize @car = '' end def build_car_frame car << "This is a standard car\n" end def add_engine car << " with an engine\n" end def add_front_wheels car << " with two front wheels\n" end def add_back_wheels car << " with two back wheels\n" end def add_dashboard car << " with a dashboard\n" end def add_energy_source car << " with a fuel tank\n" end end
The new version of the StandardCarBuilder
creates an empty car and provides six methods, build_car_frame
, add_engine
, add_front_wheels
, add_back_wheels
, add_dashboard
, and add_energy_source
, that can be used to build a car.
Similarly, the new version of the SportsCarBuilder
and ElectronicCarBuilder
look like the following.
class SportsCarBuilder attr_reader :car def initialize @car = '' end def build_car_frame car << "This is a sport car\n" end def add_engine car << " with a powerful engine\n" end def add_front_wheels car << " with two front wheels suitable for mountain paths\n" end def add_back_wheels car << " with two back wheels suitable for mountain paths\n" end def add_dashboard car << " with a fancy sporty dashboard\n" end def add_energy_source car << " with a big fuel tank\n" end end
class ElectronicCarBuilder attr_reader :car def initialize @car = '' end def build_car_frame car << "This is an electronic car\n" end def add_engine car << " with a electronic engine\n" end def add_front_wheels car << " with two front wheels\n" end def add_back_wheels car << " with two back wheels\n" end def add_dashboard car << " with a dashboard showing current battery level\n" end def add_energy_source car << " with a battery\n" end end
With these three builders, all we need is a class that uses a builder to construct a car.
class CarConstructionDirector def construct_car(builder:) builder.build_car_frame builder.add_engine builder.add_front_wheels builder.add_back_wheels builder.add_dashboard builder.add_energy_source builder.car end end
The CarConstructionDirector
has a construct_car
method that takes a builder and uses it to construct a car.
With three new car builders and a car construction director, we can construct cars like this:
First, initialize a director and builders.
Second, construct cars by passing builders to the director’s construct_car
method.
And here are the cars we create.
Revisit The Builder Pattern
The builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations.
In our car construction example, we separated the construction of a car from its representation. The construction process is in the construct_car
method of the CarConstructionDirector
. The representations of the cars are in the builders: StandardCarBuilder
, SportsCarBuilder
, and ElectronicCarBuilder
.
And we can use the same construction process, a.k.a the construct_car
method, to create different types of cars.
??Woohoo, we just learned the builder pattern!??
Keys of The Builder Pattern
There are two key parts of the builder pattern.
1. The builder pattern is used for constructing COMPLEX objects.
Using the builder pattern in our example might seem like overkill. That’s because in our example we simplified the implementations of the constructing steps. If we were actually to construct a real car, each step would be much more complicated. For example, just the add_an_engine
step alone could contain hundreds of lines of code, instead of car << “ with an engine\n”
.
If you are indeed trying to construct a complex object, consider the builder pattern. If the object you need is simple, the builder pattern might be overkill.
2. The goal of the builder pattern is to use the same construction process to create different representations of the same type of object.
In our example, we have three different representations of a car: standard, sports, and electronic.
If you have more than one representation of the object you need to construct, consider the builder pattern. Otherwise, it might be overkill.
Object-Oriented Design Concepts in The Builder Pattern
There are two important object-oriented design concepts used in the builder pattern that are worth pointing out.
1. The “encapsulate what varies” design principle.
The builder pattern uses each concrete builder (StandardCarBuilder
, SportCarBuilder
, and ElectronicCarBuilder
) to encapsulates the parts that vary.
2. Dependency Injection
Dependency injection is a technique for achieving a loose coupling between objects and their collaborators, or dependencies.
In our example, the construct_car
method has a dependency on a car builder. And we inject this dependency by passing a builder into the method.
Builder vs Template Method
The builder pattern might have reminded you of the template method pattern.
Take a look at the definition of the template method pattern. They are indeed similar.
The Template Method pattern is a behavioral design pattern that
– defines the program skeleton of an algorithm in an operation,
– deferring some steps to subclasses.
It lets one redefine certain steps of an algorithm without changing the algorithm’s structure.
They both separate the process of doing something from the implementation of each step in the process.
But they have two main differences.
1. The way they separate the process from the implementation of each step is different.
As we mentioned before, the builder pattern uses dependency injection to achieve this separation. But the template method pattern uses inheritance to achieve the separation.
2. The purpose of each pattern is different.
The builder pattern is a creational pattern specifically designed for creating objects. The template method pattern is a behavioral pattern with a more general purpose.
Now here comes an important question: which car do you want to buy? ?
Enjoyed the article?
My best content on Software Design, Rails, and Career in Dev. Delivered weekly.
Nice explanation on Builder vs Template
I rarely found a chance to use Builder pattern though