Builder Design Pattern
Creational: Builder Pattern
What is the Builder Design Pattern in JavaScript?
View Answer:
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
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:
In what pattern category does the Builder Pattern belong?
View Answer:
What is the most common reason to use the Builder Pattern?
View Answer:
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:
// 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.
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:
What are some of the benefits of using the Builder pattern?
View Answer:
- 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:
In JavaScript, Are there any alternatives to using the Builder pattern?
View Answer:
What problem does the Builder Pattern solve?
View Answer:
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:
Can you describe the key components of the Builder Pattern?
View Answer:
How does the Builder Pattern improve code maintenance?
View Answer:
What is the Director in Builder Pattern?
View Answer:
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:
How does the Builder Pattern handle optional parameters?
View Answer:
Can the Builder Pattern be used with prototype-based inheritance in JavaScript?
View Answer:
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:
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:
How is the Builder Pattern beneficial in unit testing?
View Answer:
Can Builder Pattern help with code refactoring?
View Answer:
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:
How can the Builder Pattern handle variations in object construction?
View Answer:
Can the Builder Pattern be used for creating a complex JSON object?
View Answer:
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.