Skip to main content

Builder Design Pattern

Creational: Builder Pattern



What is the Builder Design Pattern in JavaScript?

View Answer:
Interview Response: The builder design pattern is a "creational" design pattern that separates the construction of complex objects from their representation, allowing for flexible and easy object creation.

Technical Response: The Builder pattern is a design pattern that provides a flexible solution to the object construction problems. Instead of using numerous constructors, the builder pattern uses another object, a builder, that receives piece-by-piece input and completes the construction of a complex object.

Diagram:



The objects participating in this pattern are:

Director -- In example code: Shop

  • uses the Builder's multi-step interface to build products

Builder -- JavaScript does not use it.

  • asserts a multi-step interface for the creation of a complex product

ConcreteBuilder -- In example code: CarBuilder and TruckBuilder are two examples of code.

  • Implements the Builder interface with multiple steps
  • Maintains the product's integrity during the assembly process
  • offers the ability to retrieve the newly created product

Products -- In example code: Car, Truck

  • represents the complicated objects that must get assembled


Code Example:

The Builder pattern is a design pattern that provides a flexible solution to the object construction problems. Instead of using numerous constructors, the builder pattern uses another object, a builder, that receives piece-by-piece input and completes the construction of a complex object.

Here's a JavaScript example for the Builder pattern:

class CarBuilder {
constructor() {
this.car = {};
}

setMake(make) {
this.car.make = make;
return this;
}

setModel(model) {
this.car.model = model;
return this;
}

setColor(color) {
this.car.color = color;
return this;
}

setYear(year) {
this.car.year = year;
return this;
}

build() {
return this.car;
}
}

// Usage
let builder = new CarBuilder();

let car = builder
.setMake("Toyota")
.setModel("Camry")
.setColor("black")
.setYear(2023)
.build();

console.log(car);
// Output: { make: 'Toyota', model: 'Camry', color: 'black', year: 2023 }

In this example, CarBuilder is the builder. It has a setMake, setModel, setColor, and setYear method for setting the make, model, color, and year of the car respectively. Each setter method returns this to allow for method chaining. The build method is used to return the final car object.


Why use the Builder Pattern?

View Answer:
Interview Response: The Builder Pattern enhances code readability and reduces complexity by handling complex object construction and maintaining control over object assembly.

In what pattern category does the Builder Pattern belong?

View Answer:
Interview Response: The Builder Pattern belongs to the Creational design pattern category.

What is the most common reason to use the Builder Pattern?

View Answer:
Interview Response: The most common reason to use the Builder Pattern is to create complex or composite objects with many configurable parameters, while improving code readability and maintainability.

Technical Response: The most common reason for using Builder is to make client code that creates complex objects that are simpler to comprehend. The client can still direct the Builder's actions without knowing how the actual work gets completed. Because the procedures involved are frequently repetitive and complex, builders frequently encapsulate the construction of Composite objects (another GoF design pattern).

It is usually the last step that returns the newly created object, making it simple for a Builder to participate in fluent interfaces where multiple method calls separated by dot operators get chained next to each other.

Can you name the objects participating in the Builder Pattern?

View Answer:
Interview Response: In the Builder Pattern, the main participants are the Builder, which provides an interface for constructing parts of a product; the ConcreteBuilder, which implements the builder's interface, constructs and assembles the product; the Director, which constructs the object using the Builder interface; and the Product, which represents the complex object being constructed.

Code Example:

// Builder
class HouseBuilder {
setDoorType() {}
setWindowType() {}
setFloorNumber() {}
getHouse() {}
}

// Concrete Builder
class VillaBuilder extends HouseBuilder {
constructor() {
super();
this.house = {};
}

setDoorType() {
this.house.door = "Wooden Door";
return this;
}

setWindowType() {
this.house.window = "Big Window";
return this;
}

setFloorNumber() {
this.house.floor = 2;
return this;
}

getHouse() {
return this.house;
}
}

// Director
class HouseDirector {
constructHouse(builder) {
return builder
.setDoorType()
.setWindowType()
.setFloorNumber()
.getHouse();
}
}

// Usage
let director = new HouseDirector();
let house = director.constructHouse(new VillaBuilder());

console.log(house); // Output: { door: 'Wooden Door', window: 'Big Window', floor: 2 }

In this example:

1. HouseBuilder is the Builder, which defines an interface for creating parts of a house.

2. VillaBuilder is the ConcreteBuilder, which provides the implementation for the Builder. It builds and assembles the house.

3. HouseDirector is the Director, which constructs the house using the Builder's methods.

4. The house object is the Product, which represents the complex object being built.


note

Though the definition particularly mentions that an interface needs to be defined, we don’t have interfaces in Vanilla JavaScript. Therefore, we must implement it in a way that JavaScript translates into an interface.


Why is the AbstractBuilder Object participant not used in JavaScript?

View Answer:
Interview Response: In JavaScript, the AbstractBuilder object participant is not commonly used because JavaScript does not have interfaces like other object-oriented programming languages. Instead, function objects are used to implement the Builder pattern.

What are some of the benefits of using the Builder pattern?

View Answer:
Interview Response: Some benefits of using the Builder pattern in JavaScript are: improved object creation flexibility, separation of concerns, and improved code maintainability and readability.

Technical Response: The builder pattern has several advantages, which can be summarized as follows.

  • You may build items step by step, defer building phases, or perform them recursively.
  • When creating different product representations, you can reuse the same construction code.
  • Single Responsibility Principle. You may separate sophisticated building of code from the product's business logic.


What is the primary disadvantage of employing the Builder pattern?

View Answer:
Interview Response: The primary disadvantage of employing the Builder pattern in JavaScript is that it can lead to verbose code, especially when creating complex objects with many configurable parameters.

In JavaScript, Are there any alternatives to using the Builder pattern?

View Answer:
Interview Response: There are several alternatives to using the Builder pattern in JavaScript, such as using factory functions, object literals, or constructor functions. The choice of pattern depends on the specific use case and requirements.

What problem does the Builder Pattern solve?

View Answer:
Interview Response: Builder Pattern solves the 'telescoping constructor' problem where a constructor with multiple parameters becomes difficult to manage.
Technical Response: The Builder Pattern addresses the problem of constructing complex objects step-by-step. It's particularly useful when an object must be created in multiple steps, or when an object's construction requires a large number of parameters or has a complex internal structure.

Code Example:

Consider a case where we have a Pizza class and we want to be able to customize a pizza with various toppings, size, crust type, etc. If we had to pass these properties in a constructor, it could quickly become unwieldy and prone to error. The Builder Pattern can help us in such a case:

// Pizza Class
class Pizza {
constructor(builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.bacon = builder.bacon;
}
}

// Pizza Builder
class PizzaBuilder {
constructor(size) {
this.size = size;
}

addCheese() {
this.cheese = true;
return this;
}

addPepperoni() {
this.pepperoni = true;
return this;
}

addBacon() {
this.bacon = true;
return this;
}

build() {
return new Pizza(this);
}
}

// Usage
const pizza = new PizzaBuilder(12)
.addCheese()
.addPepperoni()
.addBacon()
.build();

console.log(pizza);
// Output: Pizza { size: 12, cheese: true, pepperoni: true, bacon: true }

In this example, we use a PizzaBuilder to build a Pizza object step by step, instead of passing all the parameters in a constructor. This makes the code more readable and easier to understand. Each method in PizzaBuilder returns this, which enables method chaining.


How does the Factory Pattern compare to the Builder Pattern?

View Answer:
Interview Response: The Factory Pattern creates an instance of several derived classes, and the Builder Pattern constructs a complex object step by step and returns the result.

Can you describe the key components of the Builder Pattern?

View Answer:
Interview Response: The key components of the Builder Pattern are the Builder interface, Concrete Builder, Director, and the Product. They handle the construction of complex objects step-by-step, simplifying the creation process.

How does the Builder Pattern improve code maintenance?

View Answer:
Interview Response: By separating the construction logic from the actual object, the Builder Pattern makes it easier to add new types of objects without disturbing existing code.

What is the Director in Builder Pattern?

View Answer:
Interview Response: The Director controls the order of construction steps. It knows which builder to use to get the desired object.

Code Example:

class BurgerBuilder {
constructor(size) {
this.size = size;
this.cheese = false;
this.pepperoni = false;
this.lettuce = false;
this.tomato = false;
}

addCheese() {
this.cheese = true;
return this;
}

addPepperoni() {
this.pepperoni = true;
return this;
}

addLettuce() {
this.lettuce = true;
return this;
}

addTomato() {
this.tomato = true;
return this;
}

build() {
return new Burger(this);
}
}

class Burger {
constructor(builder) {
this.size = builder.size;
this.cheese = builder.cheese || false;
this.pepperoni = builder.pepperoni || false;
this.lettuce = builder.lettuce || false;
this.tomato = builder.tomato || false;
}
}

// Director
class BurgerDirector {
createCheeseBurger(builder) {
return builder
.addCheese()
.addLettuce()
.addTomato()
.build();
}
}

// Usage
const burgerBuilder = new BurgerBuilder(14);
const burgerDirector = new BurgerDirector();

const cheeseBurger = burgerDirector.createCheeseBurger(burgerBuilder);
console.log(cheeseBurger);
// Output: Burger { size: 14, cheese: true, pepperoni: false, lettuce: true, tomato: true }

Is the Builder Pattern suitable for immutable objects? Why or why not?

View Answer:
Interview Response: Yes, it is suitable. Builder Pattern allows setting all parameters in the constructor, resulting in the creation of an immutable object.

How does the Builder Pattern handle optional parameters?

View Answer:
Interview Response: It allows setting only required parameters, enabling it to handle optional parameters more conveniently than a lengthy constructor.

Can the Builder Pattern be used with prototype-based inheritance in JavaScript?

View Answer:
Interview Response: Yes, although JavaScript uses prototype-based inheritance, the Builder Pattern can be implemented effectively to construct complex objects.

Code Example:

function Car() {
this.color = 'red';
this.doors = 4;
}

Car.prototype.setColor = function(color) {
this.color = color;
return this;
};

Car.prototype.setDoors = function(doors) {
this.doors = doors;
return this;
};

// Usage
var myCar = new Car();
myCar.setColor('blue').setDoors(2);

console.log(myCar);
// Output: Car { color: 'blue', doors: 2 }

In this example, we're using JavaScript's prototype-based inheritance to add the builder methods setColor and setDoors to instances of Car. We're also taking advantage of the ability to chain methods by returning this from each builder method. This allows us to set the color and number of doors on myCar in a fluent manner.


Can the Builder pattern be used with the Singleton Pattern?

View Answer:
Interview Response: Yes, it is possible to use the Builder Pattern in conjunction with the Singleton Pattern. The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it.

Code Example:

Singleton: This is a singleton...

class Singleton {
constructor(data) {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.data = data;
}

getData() {
return this.data;
}
}

// Usage
const singleton1 = new Singleton("Data 1");
console.log(singleton1.getData()); // Output: Data 1

const singleton2 = new Singleton("Data 2");
console.log(singleton2.getData()); // Output: Data 1

console.log(singleton1 === singleton2); // Output: true

Builder pattern be used with the Singleton Pattern:

class Database {
constructor(builder) {
this.host = builder.host;
this.port = builder.port;
this.username = builder.username;
this.password = builder.password;
}
}

class DatabaseBuilder {
setHost(host) {
this.host = host;
return this;
}

setPort(port) {
this.port = port;
return this;
}

setUsername(username) {
this.username = username;
return this;
}

setPassword(password) {
this.password = password;
return this;
}

build() {
if (!Database.instance) {
Database.instance = new Database(this);
}
return Database.instance;
}
}

// Usage
let builder = new DatabaseBuilder();
let database = builder.setHost("localhost")
.setPort(27017)
.setUsername("admin")
.setPassword("admin")
.build();

console.log(database);
// Output: Database { host: 'localhost', port: 27017, username: 'admin', password: 'admin' }

In this example, the Database class is a Singleton class that uses the DatabaseBuilder to configure its only instance. The DatabaseBuilder implements the Builder pattern to allow for step-by-step creation and configuration of the Database instance. The build method of DatabaseBuilder checks if Database.instance already exists before creating a new one, ensuring only one Database instance ever exists.


How does the Builder Pattern enhance code readability?

View Answer:
Interview Response: It breaks down the object creation process into smaller, more readable steps, and allows the creation of different kinds of objects using the same construction process.

How is the Builder Pattern beneficial in unit testing?

View Answer:
Interview Response: By isolating the construction of an object, the Builder Pattern enables easier and more precise control in unit testing scenarios.

Can Builder Pattern help with code refactoring?

View Answer:
Interview Response: The Builder Pattern can significantly help with code refactoring, especially when dealing with constructors or functions that take many parameters. By separating construction from representation, it can help simplify code, making refactoring easier and be less error-prone.

Code Example:

Imagine you have a User class that takes a large number of parameters in its constructor.

class User {
constructor(firstName, lastName, age, address, phone, email) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
this.phone = phone;
this.email = email;
}
}

// Usage
let user = new User("John", "Doe", 30, "123 Street", "1234567890", "john@example.com");

Over time, as the number of parameters increases, the constructor becomes more complicated, and it's easier to make a mistake when instantiating the class. By using the Builder Pattern, you can simplify the code and make it more readable:

class UserBuilder {
setName(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
return this;
}

setAge(age) {
this.age = age;
return this;
}

setAddress(address) {
this.address = address;
return this;
}

setContact(phone, email) {
this.phone = phone;
this.email = email;
return this;
}

build() {
return new User(this);
}
}

class User {
constructor(builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
this.email = builder.email;
}
}

// Usage
let user = new UserBuilder()
.setName("John", "Doe")
.setAge(30)
.setAddress("123 Street")
.setContact("1234567890", "john@example.com")
.build();

console.log(user);
// Output: User { firstName: 'John', lastName: 'Doe', age: 30, address: '123 Street', phone: '1234567890', email: 'john@example.com' }

In this refactored example, the UserBuilder provides a fluent interface for creating a User. Each method of UserBuilder sets one or more properties and returns the builder object to allow for method chaining. The build method is used to return the final User object. This makes the code more maintainable, easier to read, and reduces the likelihood of mistakes when creating a User.


When should the Builder Pattern be avoided?

View Answer:
Interview Response: The Builder Pattern should be avoided when the object construction process is simple, or there aren't many parameters. Overuse can lead to unnecessary code complexity and may negatively impact performance.

How can the Builder Pattern handle variations in object construction?

View Answer:
Interview Response: The Builder Pattern handles variations by abstracting the construction process and allowing different implementations for each step, hence creating different representations of an object.

Can the Builder Pattern be used for creating a complex JSON object?

View Answer:
Interview Response: Yes, the Builder Pattern can be used to create a complex JSON object by defining different builders for different parts of the JSON object.

Code Example:

class JsonObjectBuilder {
constructor() {
this.jsonObject = {};
}

addProperty(key, value) {
this.jsonObject[key] = value;
return this;
}

addNestedObject(key) {
this.jsonObject[key] = new JsonObjectBuilder();
return this.jsonObject[key];
}

build() {
return JSON.stringify(this.jsonObject);
}
}

// Usage
let jsonObjectBuilder = new JsonObjectBuilder();

jsonObjectBuilder
.addProperty("name", "John")
.addProperty("age", 30);

let addressBuilder = jsonObjectBuilder.addNestedObject("address");

addressBuilder
.addProperty("street", "123 Street")
.addProperty("city", "New York")
.addProperty("state", "NY");

console.log(jsonObjectBuilder.build());
// Output: {"name":"John","age":30,"address":{"street":"123 Street","city":"New York","state":"NY"}}

In this example, the JsonObjectBuilder provides a fluent interface for creating a JSON object. The addProperty method adds a property to the JSON object, while the addNestedObject method adds a nested object, returning a new builder for that nested object. The build method returns the JSON string representation of the constructed object.


How does the Builder Pattern support the Open-Closed Principle?

View Answer:
Interview Response: By allowing addition of new steps in the object creation process without modifying the existing code, Builder Pattern supports the Open-Closed Principle.

What's a practical use case for the Builder Pattern in JavaScript?

View Answer:
Interview Response: A common use case is when creating complex DOM structures or large JSON objects where we need to ensure a specific build process.

How does Builder Pattern relate to the Single Responsibility Principle?

View Answer:
Interview Response: The Builder Pattern adheres to the Single Responsibility Principle as each builder is only responsible for the construction of a single type of object.