Design Pattern: Builder - using GoLang

Jayaprasanna Roddam - Nov 6 - - Dev Community

The Builder pattern is a creational design pattern used to construct complex objects step-by-step, separating the construction process from the actual object representation. This pattern is particularly helpful when an object requires multiple steps or options for configuration, or if many optional parameters would make constructor overloads confusing or unwieldy.

Builder Pattern in Go

Since Go doesn’t support method chaining with "self-returning" classes as naturally as languages like Java or C#, it uses a more functional approach, but the principles remain the same. In Go, the Builder pattern typically involves:

  1. Defining a Builder struct to manage the construction.
  2. Using methods to set parameters of the object being constructed.
  3. A Build method that returns the final constructed object.

Example Scenario: Building a House

Let’s say we want to build a House struct, but the construction process can vary. For example, a house might have a number of rooms, a garage, a garden, or even a swimming pool, and each option can be configured independently.

Step 1: Define the Product

The House struct represents the object we’re constructing. It has various fields representing different components.

package main

import "fmt"

// House represents the final product with multiple configurable parts.
type House struct {
    Rooms       int
    HasGarage   bool
    HasGarden   bool
    HasPool     bool
    Description string
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Define the Builder

The HouseBuilder struct will be used to configure the House instance step-by-step. It has methods to set each component individually.

// HouseBuilder is responsible for constructing a House step-by-step.
type HouseBuilder struct {
    rooms       int
    hasGarage   bool
    hasGarden   bool
    hasPool     bool
    description string
}

// NewHouseBuilder initializes a new HouseBuilder with default values.
func NewHouseBuilder() *HouseBuilder {
    return &HouseBuilder{}
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Builder Methods

Each method in the builder sets a specific property for the house. Each setter method returns the builder itself, allowing us to chain the calls.

// SetRooms sets the number of rooms in the house.
func (b *HouseBuilder) SetRooms(rooms int) *HouseBuilder {
    b.rooms = rooms
    return b
}

// AddGarage adds a garage to the house.
func (b *HouseBuilder) AddGarage() *HouseBuilder {
    b.hasGarage = true
    return b
}

// AddGarden adds a garden to the house.
func (b *HouseBuilder) AddGarden() *HouseBuilder {
    b.hasGarden = true
    return b
}

// AddPool adds a swimming pool to the house.
func (b *HouseBuilder) AddPool() *HouseBuilder {
    b.hasPool = true
    return b
}

// SetDescription sets a custom description for the house.
func (b *HouseBuilder) SetDescription(description string) *HouseBuilder {
    b.description = description
    return b
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Build the Final Product

The Build method is the final step in the Builder pattern, constructing and returning the House object based on the values set in the builder.

// Build constructs the final House object with the specified configurations.
func (b *HouseBuilder) Build() House {
    return House{
        Rooms:       b.rooms,
        HasGarage:   b.hasGarage,
        HasGarden:   b.hasGarden,
        HasPool:     b.hasPool,
        Description: b.description,
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Using the Builder in Client Code

In the main function, we create an instance of HouseBuilder, set the desired properties, and finally call Build to get the House object. This allows for flexible and readable object construction.

func main() {
    // Using the builder to create a house with specific configurations.
    house := NewHouseBuilder().
        SetRooms(3).
        AddGarage().
        AddGarden().
        SetDescription("A cozy family home with modern amenities.").
        Build()

    fmt.Println(house)
    // Output: {3 true true false A cozy family home with modern amenities.}
}
Enter fullscreen mode Exit fullscreen mode

Explanation of the Builder Pattern in Go Terms

  1. Product (House): This struct represents the complex object we’re constructing. It has multiple optional fields (e.g., Rooms, HasGarage, HasGarden, etc.).

  2. Builder (HouseBuilder): This struct manages the construction of the House object, allowing properties to be set step-by-step. Each method in HouseBuilder sets a specific property and returns the builder itself, supporting method chaining.

  3. Build Method: The Build method constructs the final product (House) with the specified properties.

  4. Usage: The client code only interacts with the builder, making the process of constructing a House flexible and easy to read. By chaining setter methods, we can customize the object with various options.

Advantages of the Builder Pattern

  • Clarity and Flexibility: The Builder pattern provides a clear way to construct objects with numerous optional configurations.
  • Immutable Product: Once a House is built, it’s immutable and safe from unintended changes.
  • Scalability: If additional properties are added to the House, the HouseBuilder can be extended with new methods without affecting existing client code.

Reason for using the same fields in both House and HouseBuilder structs:
Temporary Storage for Step-by-Step Configuration: The builder struct temporarily holds values for the object it’s building. Each method in the builder sets a field or configures an option, and the builder accumulates these values until all necessary settings are applied. When you call Build(), the builder transfers its stored values to create the final House instance. This allows for a step-by-step, fluent interface to set fields.

Immutability of the Final Object: The final object (House) can remain immutable if constructed with all parameters at once. By separating the configuration phase (handled by HouseBuilder) from the final object creation, the House instance itself is protected from later modifications, making it safe and consistent after construction.

Avoiding a Complex Constructor: If the House struct has many optional fields or complex initialization logic, using a builder struct prevents the need for multiple constructors or overly complex struct initialization. With the builder, you don’t need to pass all options upfront; instead, you set each property as needed in a readable, chainable way.

Clearer Separation of Responsibilities: The builder struct isolates the configuration logic and provides flexibility. This allows the final product to remain focused on representing the "built" object without carrying construction logic.

Practical Use Cases for the Builder Pattern

The Builder pattern is useful when:

  • The object has many configurable options or attributes.
  • There are multiple optional properties, and you want to avoid multiple constructor arguments.
  • You want the code to be flexible, readable, and maintainable, especially when constructing complex objects with conditional configurations.

In Go, this pattern is particularly helpful when you want to construct objects with multiple configurations but avoid complex struct initializations with multiple parameters. It keeps code readable and supports chaining, making it easier to understand and modify.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .