Builder Pattern | Deep Dive In Design Patterns

Narendra Singh Rathore
6 min readAug 15, 2024

--

The Builder pattern is a design pattern used to construct a complex object step by step. It allows you to create different representations of an object using the same construction process. This pattern is especially useful when you need to construct an object with many optional components or when the construction process is complex.

Advantages

  1. Separation of Concerns: It separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
  2. Immutability: The builder pattern often encourages immutability of the constructed object, which can lead to more predictable and bug-free code.
  3. Flexibility: It provides a way to construct complex objects step-by-step and to construct objects with different representations.

Disadvantages

  1. Complexity: It can introduce additional complexity and boilerplate code, which might not be justified for simple objects.
  2. Overhead: In cases where objects are not complex, the Builder pattern might add unnecessary overhead compared to a simple constructor.

Code Sample

Here’s a TypeScript example demonstrating the Builder pattern:

// Product class with many optional parameters
class Car {
private make: string;
private model: string;
private year: number;
private color: string;
private isElectric: boolean;

constructor(builder: CarBuilder) {
this.make = builder.make;
this.model = builder.model;
this.year = builder.year;
this.color = builder.color;
this.isElectric = builder.isElectric;
}

public getDescription(): string {
return `${this.year} ${this.make} ${this.model} (${this.color}, Electric: ${this.isElectric})`;
}
}

// Builder class for the Car
class CarBuilder {
public make: string;
public model: string;
public year: number;
public color: string;
public isElectric: boolean;

constructor(make: string, model: string) {
this.make = make;
this.model = model;
// Default values
this.year = 2024;
this.color = 'black';
this.isElectric = false;
}

public setYear(year: number): CarBuilder {
this.year = year;
return this;
}

public setColor(color: string): CarBuilder {
this.color = color;
return this;
}

public setElectric(isElectric: boolean): CarBuilder {
this.isElectric = isElectric;
return this;
}

public build(): Car {
return new Car(this);
}
}

// Usage
const carBuilder = new CarBuilder('Tesla', 'Model S')
.setYear(2024)
.setColor('red')
.setElectric(true);

const myCar = carBuilder.build();
console.log(myCar.getDescription()); // Output: 2024 Tesla Model S (red, Electric: true)

How It Improves Code

  • Clarity: By using the Builder pattern, you clearly define what parameters are required and what parameters are optional, leading to more readable code.
  • Flexibility: You can easily create different variations of the object (e.g., different car models or colors) without changing the construction process.
  • Immutability: The constructed object is immutable, making it less error-prone and more reliable.

The Builder pattern is a good choice when you need to construct complex objects with many optional components, providing clarity and flexibility in the object construction process.

Real-World Use Case: User Profile Builder

Imagine an application where users can create their profiles with various attributes, such as username, email, phone number, address, and preferences. The Builder pattern is useful here because it allows users to build their profiles with different combinations of attributes without cluttering the code with multiple constructors or setters.

Code Sample

// UserProfile class with many optional parameters
class UserProfile {
private username: string;
private email: string;
private phoneNumber?: string;
private address?: string;
private preferences: string[];

constructor(builder: UserProfileBuilder) {
this.username = builder.username;
this.email = builder.email;
this.phoneNumber = builder.phoneNumber;
this.address = builder.address;
this.preferences = builder.preferences;
}

public getProfileInfo(): string {
return `UserProfile: ${this.username}, ${this.email}, Phone: ${this.phoneNumber ?? 'N/A'}, Address: ${this.address ?? 'N/A'}, Preferences: ${this.preferences.join(', ')}`;
}
}

// Builder class for UserProfile
class UserProfileBuilder {
public username: string;
public email: string;
public phoneNumber?: string;
public address?: string;
public preferences: string[] = [];

constructor(username: string, email: string) {
this.username = username;
this.email = email;
}

public setPhoneNumber(phoneNumber: string): UserProfileBuilder {
this.phoneNumber = phoneNumber;
return this;
}

public setAddress(address: string): UserProfileBuilder {
this.address = address;
return this;
}

public addPreference(preference: string): UserProfileBuilder {
this.preferences.push(preference);
return this;
}

public build(): UserProfile {
return new UserProfile(this);
}
}

// Usage
const profileBuilder = new UserProfileBuilder('john_doe', 'john.doe@example.com')
.setPhoneNumber('123-456-7890')
.setAddress('123 Elm Street')
.addPreference('Dark Mode')
.addPreference('Email Notifications');

const userProfile = profileBuilder.build();
console.log(userProfile.getProfileInfo());
// Output: UserProfile: john_doe, john.doe@example.com, Phone: 123-456-7890, Address: 123 Elm Street, Preferences: Dark Mode, Email Notifications

Explanation

  1. UserProfile Class: Represents a complex user profile with several attributes. Some attributes are optional, such as phoneNumber and address.
  2. UserProfileBuilder Class: Provides a step-by-step approach to constructing a UserProfile. It allows setting required attributes (username, email) and optional attributes (phoneNumber, address) in a fluent way. The addPreference method allows adding multiple preferences.
  3. Usage: The UserProfileBuilder is used to construct a UserProfile with various attributes and preferences. This approach makes the code more readable and maintainable compared to having multiple constructors or a single constructor with many parameters.

How It Improves Code

  • Flexibility: Users can build profiles with different combinations of attributes and preferences without needing multiple constructors or complex logic.
  • Readability: The fluent API provided by the builder makes it clear which attributes are being set, enhancing code readability.
  • Maintainability: Adding or modifying attributes in the UserProfile class does not require changing the existing client code that uses the builder.

This real-world example illustrates how the Builder pattern can simplify the creation of complex objects and improve code organization.

Let’s explore a real-world use case where the Builder pattern can significantly improve code quality and maintainability.

Use Case: Building a Complex Document Report

Imagine you’re developing a system to generate complex document reports with various sections and configurations. Each report may include different combinations of sections like a title, table of contents, main content, and footer. The Builder pattern helps manage the complexity of constructing such documents by allowing incremental and flexible composition.

Scenario Without Builder Pattern

In a typical approach without the Builder pattern, you might have a constructor with numerous parameters, or multiple methods to set up each part of the document, which can be cumbersome and error-prone:

class DocumentReport {
private title: string;
private tableOfContents?: string;
private content: string;
private footer?: string;

constructor(title: string, content: string, tableOfContents?: string, footer?: string) {
this.title = title;
this.content = content;
this.tableOfContents = tableOfContents;
this.footer = footer;
}

public getReport(): string {
return `
Title: ${this.title}
${this.tableOfContents ? `Table of Contents: ${this.tableOfContents}` : ''}
Content: ${this.content}
${this.footer ? `Footer: ${this.footer}` : ''}
`;
}
}

// Usage
const report = new DocumentReport('Annual Report', 'Content of the report', 'Chapter 1, Chapter 2', 'Confidential');
console.log(report.getReport());

Issues with the Typical Approach

  • Complex Constructors: When adding new sections or options, the constructor becomes complex and less readable.
  • Error-Prone: It’s easy to mix up parameters, especially when many are optional.
  • Lack of Flexibility: Adding or removing sections requires changes to the constructor and handling optional parameters.

Improved Approach Using the Builder Pattern

Using the Builder pattern, you can improve the code by clearly defining which parts of the document are optional and providing a fluent API for building the document step-by-step.

class DocumentReport {
private title: string;
private tableOfContents?: string;
private content: string;
private footer?: string;

constructor(builder: DocumentReportBuilder) {
this.title = builder.title;
this.content = builder.content;
this.tableOfContents = builder.tableOfContents;
this.footer = builder.footer;
}

public getReport(): string {
return `
Title: ${this.title}
${this.tableOfContents ? `Table of Contents: ${this.tableOfContents}` : ''}
Content: ${this.content}
${this.footer ? `Footer: ${this.footer}` : ''}
`;
}
}

class DocumentReportBuilder {
public title: string;
public content: string;
public tableOfContents?: string;
public footer?: string;

constructor(title: string, content: string) {
this.title = title;
this.content = content;
}

public setTableOfContents(tableOfContents: string): DocumentReportBuilder {
this.tableOfContents = tableOfContents;
return this;
}

public setFooter(footer: string): DocumentReportBuilder {
this.footer = footer;
return this;
}

public build(): DocumentReport {
return new DocumentReport(this);
}
}

// Usage
const reportBuilder = new DocumentReportBuilder('Annual Report', 'Content of the report')
.setTableOfContents('Chapter 1, Chapter 2')
.setFooter('Confidential');

const report = reportBuilder.build();
console.log(report.getReport());

How the Builder Pattern Improves Code

  1. Readability: The builder pattern provides a fluent API that clearly shows which parts of the document are being set, improving readability.
  2. Flexibility: You can easily add or modify sections without changing the constructor or existing client code.
  3. Maintainability: Adding new sections or options involves adding methods to the builder class rather than modifying the constructor or handling many optional parameters.
  4. Immutability: Once built, the DocumentReport object is immutable, ensuring that its state remains consistent.

Summary

The Builder pattern enhances the construction of complex objects by:

  • Offering a clear and flexible way to set optional parameters.
  • Improving readability and maintainability.
  • Reducing the risk of errors in object creation.

In scenarios where objects have many optional parts or configurations, like generating complex reports, the Builder pattern provides a more manageable and scalable solution.

Thanks for reading.

--

--

No responses yet