Friday, January 3, 2025

Builder

Profile Pic of Akash AmanAkash Aman

Updated: May 2025

πŸ“ Brief

The Builder pattern is a creational design pattern πŸ—οΈ that separates the construction of a complex object from its representation. This allows for creating different representations of an object using the same construction process.

Imagine assembling a computer. Instead of handling all the details at once (processor, memory, storage, etc.), you have a β€œbuilder” that handles the step-by-step assembly, giving you flexibility to create variations like gaming PCs, workstations, or budget systems.

❓ Problem

The Builder pattern solves two key problems:

  • πŸ—οΈ Managing complex object creation.

    Complex objects often require detailed, step-by-step initialization of multiple fields and nested objects. This initialization is typically buried inside:

    • A large, unwieldy constructor with too many parameters (commonly referred to as the "telescoping constructor" problem).
    • Or worse, scattered across the client code, making it hard to read, maintain, or debug.

    The Builder pattern organizes this process into manageable steps.

  • πŸ”„ Creating different representations.

    Sometimes, you need different versions of the same object with varying configurations. one might try creating multiple subclasses to cover all possible variations. This approach becomes unmanageable when:

    • New features or parameters are introduced (e.g., adding a swimming pool or porch style to a house).
    • Every combination of parameters leads to a growing hierarchy of subclasses.

    The Builder pattern simplifies this by encapsulating the construction logic.

πŸ’‘ Solution

Let’s explore how the Builder pattern achieves flexibility and clarity:

  1. Define a Builder Interface:
    The first step is to define a blueprint for building objects. This interface provides the structure for the steps needed to assemble an object. It ensures that all concrete builders follow the same process.

  2. Implement Concrete Builders:
    Concrete builders implement the defined steps to construct specific object variations. This allows for creating different configurations or representations of the object.

  3. Direct the Building Process:
    A director class orchestrates the building process. It ensures that builders follow the correct sequence of steps while allowing customization of the process.

  4. Simplify Object Construction:
    The client code interacts with the director or builder to request specific configurations, abstracting away the complexity of the construction process.

πŸ“Š Blueprint

Builder PatternAll methods within a class return instances of themselves.Class InstanceMethodsSince all methods in a class return objects of the same class, we can easily implement method chaining by calling one method after another on the returned instance.

go
class Person { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, my name is ${this.name}!`); return this; // returns the instance to enable chaining } sayGoodbye() { console.log(`Goodbye, ${this.name}!`); return this; // returns the instance to enable chaining } } const person = new Person('John'); person.sayHello().sayGoodbye(); // outputs: Hello, my name is John!, followed by Goodbye, John!

πŸ› οΈ Implementation & Analysis

There are two main approaches to implement the Builder pattern: Standard Builder & Fluent Builder.

Note:
  • A Standard Builder approach uses discrete methods to set object properties step by step, offering clarity and simplicity.

  • The Fluent Builder style chains method calls for a more concise and readable configuration. This approach is especially helpful when constructing objects with many configurable attributes.

  • Define Builder Interface: Create a base interface or abstract class with methods for setting the components of the object. This ensures that all concrete builders follow a consistent structure.

  • Concrete Builders: Implement the builder interface in classes tailored to create specific configurations or representations of the object. These builders encapsulate the construction logic.

  • Director Role: Introduce a director class to control the sequence of steps for creating the object. While optional, this ensures consistency when assembling complex objects.

    For simpler use cases, the client can directly interact with the builder without requiring a director.

  • Client Code Integration: Use the builder or director to construct objects. This separates the construction logic from the object itself, improving code maintainability and flexibility.

Let's understand with an example.

In this example, we’ll implement a Meal Builder using the Builder pattern. The Builder pattern helps us construct complex objects step-by-step. Our goal is to create a Meal that consists of multiple items (like a Burger and a Drink) while keeping the construction process independent of the actual meal structure.

Step 1: Defining the Product

The "product" in the Builder pattern is the object being built. In our example, it’s the Meal.

go
type Meal struct { items []string } func (m *Meal) AddItem(item string) { m.items = append(m.items, item) } func (m *Meal) ShowItems() { fmt.Println("Meal includes:") for _, item := range m.items { fmt.Println(" -", item) } }
  • AddItem: Adds an item to the meal.
  • ShowItems: Displays all the items in the meal.

Step 2: Creating the Builder Interface

The Builder interface defines the steps required to build a meal. Each concrete builder will implement this interface to provide specific implementations for the steps.

go
type MealBuilder interface { AddBurger() AddDrink() GetMeal() *Meal }
  • AddBurger and AddDrink: These methods define the steps for adding items to the meal.
  • GetMeal: Returns the constructed Meal.

Step 3: Implementing Concrete Builders

Let’s create two concrete builders: VegMealBuilder for vegetarian meals and NonVegMealBuilder for non-vegetarian meals.

go
type VegMealBuilder struct { meal *Meal } func NewVegMealBuilder() *VegMealBuilder { return &VegMealBuilder{meal: &Meal{}} } func (b *VegMealBuilder) AddBurger() *VegMealBuilder { b.meal.AddItem("Veg Burger") return b } func (b *VegMealBuilder) AddDrink() *VegMealBuilder { b.meal.AddItem("Orange Juice") return b } func (b *VegMealBuilder) GetMeal() *Meal { return b.meal } type NonVegMealBuilder struct { meal *Meal } func NewNonVegMealBuilder() *NonVegMealBuilder { return &NonVegMealBuilder{meal: &Meal{}} } func (b *NonVegMealBuilder) AddBurger() *NonVegMealBuilder { b.meal.AddItem("Chicken Burger") return b } func (b *NonVegMealBuilder) AddDrink() *NonVegMealBuilder { b.meal.AddItem("Coke") return b } func (b *NonVegMealBuilder) GetMeal() *Meal { return b.meal }

Step 4: Creating the Director (Optional)

The Director is responsible for managing the construction process. It uses a builder to construct a Meal step-by-step.

go
type Director struct { builder MealBuilder } func (d *Director) SetBuilder(builder MealBuilder) *Director { d.builder = builder return d } func (d *Director) ConstructMeal() *Director { d.builder.AddBurger().AddDrink() return d }

Step 5: Using the Builder Pattern in the Main Function

Now, let’s see the Builder pattern in action in the main function.

go
func main() { vegBuilder := NewVegMealBuilder() nonVegBuilder := NewNonVegMealBuilder() director := &Director{} // Build a vegetarian meal director.SetBuilder(vegBuilder).ConstructMeal() vegMeal := vegBuilder.GetMeal() fmt.Println("Vegetarian Meal:") vegMeal.ShowItems() // Build a non-vegetarian meal director.SetBuilder(nonVegBuilder).ConstructMeal() nonVegMeal := nonVegBuilder.GetMeal() fmt.Println("Non-Vegetarian Meal:") nonVegMeal.ShowItems() }

What Happens Here?

  • We create two builders: VegMealBuilder and NonVegMealBuilder.
  • The Director orchestrates the construction process using the builder set via SetBuilder.
  • The constructed meals are retrieved using GetMeal and displayed using ShowItems.

πŸ§‘β€πŸ’» Putting It All Together

Here’s the complete Builder implementation:

go
package main import ( "fmt" ) type Meal struct { items []string } func (m *Meal) AddItem(item string) { m.items = append(m.items, item) } func (m *Meal) ShowItems() { fmt.Println("Meal includes:") for _, item := range m.items { fmt.Println(" -", item) } } // Builder Interface type MealBuilder interface { AddBurger() *MealBuilder AddDrink() *MealBuilder GetMeal() *Meal } // Concrete Builders type VegMealBuilder struct { meal *Meal } func NewVegMealBuilder() *VegMealBuilder { return &VegMealBuilder{meal: &Meal{}} } func (b *VegMealBuilder) AddBurger() *VegMealBuilder { b.meal.AddItem("Veg Burger") return b } func (b *VegMealBuilder) AddDrink() *VegMealBuilder { b.meal.AddItem("Orange Juice") return b } func (b *VegMealBuilder) GetMeal() *Meal { return b.meal } type NonVegMealBuilder struct { meal *Meal } func NewNonVegMealBuilder() *NonVegMealBuilder { return &NonVegMealBuilder{meal: &Meal{}} } func (b *NonVegMealBuilder) AddBurger() *NonVegMealBuilder { b.meal.AddItem("Chicken Burger") return b } func (b *NonVegMealBuilder) AddDrink() *NonVegMealBuilder { b.meal.AddItem("Coke") return b } func (b *NonVegMealBuilder) GetMeal() *Meal { return b.meal } // Director type Director struct { builder MealBuilder } func (d *Director) SetBuilder(builder MealBuilder) *Director { d.builder = builder return d } func (d *Director) ConstructMeal() *Director { d.builder.AddBurger().AddDrink() return d } func main() { vegBuilder := NewVegMealBuilder() nonVegBuilder := NewNonVegMealBuilder() director := &Director{} // Build a vegetarian meal director.SetBuilder(vegBuilder).ConstructMeal() vegMeal := vegBuilder.GetMeal() fmt.Println("Vegetarian Meal:") vegMeal.ShowItems() // Build a non-vegetarian meal director.SetBuilder(nonVegBuilder).ConstructMeal() nonVegMeal := nonVegBuilder.GetMeal() fmt.Println("Non-Vegetarian Meal:") nonVegMeal.ShowItems() }

βš–οΈ Pros & Cons

πŸš€ ProsπŸ‘ŽπŸ» Cons
You can be sure that a class has only a single instance.Violates the Single Responsibility Principle. The pattern solves two problems at the time.

πŸ•’ When to Use

The Builder pattern is especially useful when:
  • Objects require numerous configuration steps or optional parameters.
  • Different representations of an object are needed with minimal effort.

However, if your object is simple and doesn’t involve many parameters, the Builder pattern may add unnecessary complexity.