Recently, I was exploring design patterns courses on my LinkedIn Learning subscription. I came across a course, Node.js: Design Patterns
by Alex Banks
. It is a wonderful, easy-to-understand course. I started with the Builder Pattern, and the explanation was excellent, so I thought, why not try implementing it in C++? In this blog, I will explain what the “Builder Pattern” is and show its implementation in JavaScript, similar to Alex’s example, to understand it better. Later, I will implement it in C++.
What is a Builder Pattern?
The Builder Pattern is a creational design pattern that provides a flexible solution for constructing complex objects. In other words, it simplifies object construction. It allows for the step-by-step creation of objects, enabling the customization of the object construction process. For example, when constructing a Person object, instead of providing long parameters to the constructor (as a configuration option), we can allow clients to configure the Person objects one piece at a time.
Imagine you are passing a list of parameters like booleans and strings such as isManager
, isEmployee
, hours
, and name
to construct a user object. You would pass these parameters like this: Person(true, true, 40, "Julie")
. First of all, this looks very messy, and you need to know the order in which these parameters are expected in the Person's constructor. We can improve this with the Builder Pattern.
Why use Builder Pattern?
Two of the most common doubts I see on the internet, and I also had this in the beginning, are: “Can’t we directly pass an object and, based on that, create an object and add everything within a single Person class instead of using the Builder Pattern?”. Another question is, "Can’t we use a Typescript interface to enforce the object creation according to the defined interface, something like this?”
interface PersonModal {
name: String;
isEmployee: boolean;
isManager?:Boolean;
hours?: Number;
}
The answer to both questions is yes, you can do it, but the Builder Pattern makes the code more readable and easy to use. Its usage is like a Lego box where you have different parts, and you can construct an object as per your need.
You give the responsibility for object creation to a separate builder class instead of its definition class. You just have to add dot notation and method name, and you can add a feature to your object creation. Its main usage is the separation of the construction of complex objects from their representation (official definition). Imagine you are dumping all complex logic in the same representation class, i.e., the Person
class. It will eventually become tough to manage. Refer to the pseudo-code to understand what I mean by plug-and-play and how can we construct and object.
Person -- make --> Manager() -- make --> partTimeEmployee()
In code, it looks like this:
new PersonBuilder()
.makeEmployee()
.makeManager()
.build("Julie");
When not to use Builder Pattern?
Till now we’ve seen all good things about Builder Pattern and how it can be help in constructing complex objects. There are few scenario’s where we cannot use (or avoid it). Below are the few points:
When the object is simple, with few attributes.
When performance is critical because this pattern introduce overhead because of multiple function calls for each attribute assignment.
When object structure is stable or unlikely to change builder pattern can be overkill.
Code Examples in Javascript and C++
Javascript version
// Person.js
class Person {
constructor(builder) {
this.name = builder.name;
this.isEmployee = builder.isEmployee;
this.isManager = builder.isManager;
this.hours = builder.hours || 0;
}
toString() {
return JSON.stringify(this);
}
}
export default Person;
// PersonBuilder.js
import Person from "./Person.js";
class PersonBuilder {
makeEmployee() {
this.isEmployee = true;
return this;
}
makeManager() {
this.isManager = true;
return this;
}
addShoppingList(list) {
this.shoppingList = list;
return this;
}
build(name) {
this.name = name;
return new Person(this);
}
}
export default PersonBuilder;
// index.js
import PersonBuilder from "./PersonBuilder.js";
const Julie = new PersonBuilder()
.makeEmployee()
.makeManager()
.build("Julie");
console.log(Julie);
C++ version
// person.h
#include <iostream>
class PersonBuilder;
class Person {
std::string name;
bool isEmployee = false;
bool isManager = false;
int hours = 0;
friend class PersonBuilder;
public:
void printUserDetails();
};
class PersonBuilder {
Person person;
public:
PersonBuilder& setName(const std::string& name);
PersonBuilder& makeEmployee();
PersonBuilder& makeManager();
PersonBuilder& addWorkingHours(int hours);
Person build();
};
// person.cpp
#include "person.h"
PersonBuilder& PersonBuilder::makeEmployee() {
person.isEmployee = true;
return *this;
}
PersonBuilder& PersonBuilder::makeManager() {
person.isManager = true;
return *this;
}
PersonBuilder& PersonBuilder::addWorkingHours(int workingHours) {
person.hours = workingHours;
return *this;
}
PersonBuilder& PersonBuilder::setName(const std::string& name) {
person.name = name;
return *this;
}
Person PersonBuilder::build() {
return person;
}
void Person::printUserDetails() {
std::cout << "Name: " << name
<< ", isEmployee: " << isEmployee
<< ", isManager: " << isManager
<< ", Working Hours: " << hours << std::endl;
}
int main() {
Person julie = PersonBuilder()
.setName("Julie")
.makeEmployee()
.build();
julie.printUserDetails();
return 0;
}
Conclusion
The Builder Pattern is easy to understand and a good starting point for learning OOP design patterns. I’ll be writing about other design patterns in upcoming blog posts. I hope you like this blog. If you have any questions, please comment below. Thanks for reading 😊.