Design Pattern (8) Builder Pattern Complete Tutorial - Step-by-Step Construction of Complex Objects

You can download the Design Pattern series code from this design_pattern repo.

Introduction: From Factory to Customization

In the previous two articles, we explored the Factory Method Pattern and Abstract Factory Pattern, both of which focus on the problem of “what products to create.” Today, we will face a new challenge: “how to create complex products.”

Requirements: The Customization Challenge of Bubble Tea

Business Upgrade Requirements

Today we want to design an intelligent machine that can automatically make bubble tea. After market research, we found that if a bubble tea shop only sells basic black tea and green tea, it definitely cannot satisfy the diverse needs of modern consumers.

Modern customers pursue personalized experiences, and they hope to be able to:

  • Freely choose toppings: Add different toppings based on personal preferences
  • Flexible combinations: The same drink can contain multiple types of toppings
  • Personalized taste: Create their own unique flavors

Available Toppings List

We’ve decided to provide the following rich topping options to attract customers:

  • Pearls: Classic chewy texture
  • Coconut Jelly: Refreshing coconut flavor
  • Red Beans: Traditional sweet taste
  • Grass Jelly: Cool and refreshing taste
  • Pudding: Rich creamy enjoyment

Object-Oriented Analysis (OOA)

Initial Design Attempt

After understanding the requirements, let’s conduct object-oriented analysis. The intuitive approach is to add all possible topping properties to the beverage class:

Problems with the First Approach

This design brings serious problems: if we only want to add red beans and pudding today, we must pass false or null for other unused parameters.

Problem Points:

  • Lengthy Parameters: As toppings increase, the parameter list becomes very long
  • Poor Readability: Difficult to understand the meaning of each parameter
  • Maintenance Difficulties: All calling locations need modification when adding toppings
  • High Error Rate: Easy to pass wrong parameters or miss parameters

Second Approach: Multiple Constructors

You might think of using multiple different constructors to solve this, so you don’t need to pass unused parameters:

Recognizing Problems (Forces)

The Dilemma of Multiple Constructor Approach

After deep analysis of the second approach, we discovered more serious problems:

Combinatorial Explosion: As the number of toppings increases, the number of required constructors grows exponentially. With 5 types of toppings, we theoretically need 2^5 = 32 different constructors to cover all combinations!

Maintenance Nightmare:

  • Every time we add toppings, we need to significantly modify existing code
  • Constructors can easily be confused, increasing the risk of usage errors
  • The class becomes extremely large and difficult to understand

Telescoping Constructor Anti-pattern

This phenomenon is called the Telescoping Constructor anti-pattern:

Definition: When a class has multiple constructors with different numbers of parameters, leading to code that’s difficult to maintain and use.

Typical Characteristics:

  • Number of constructors grows exponentially with parameter combinations
  • High code duplication
  • Users easily choose wrong constructors
  • Extremely high maintenance costs when adding parameters

We need a more elegant solution to handle this complex object construction requirement.

Applying Builder Pattern (Solution)

Pattern Introduction

After completing object-oriented analysis (OOA), recognizing problem points (Forces), and understanding the entire problem context (Context), we can apply the Builder Pattern to solve this complex object construction problem.

Let’s first understand the standard structure of the Builder Pattern:

Core Roles of Builder Pattern

The Builder Pattern mainly includes the following five key roles:

1. Product

The final result of complex objects. It may contain multiple components or parts, whose structure varies according to different builder implementations. Product is usually a class whose attributes represent different parts constructed by the Builder.

2. Builder (Abstract Builder)

Defines the abstract interface for constructing complex objects. It declares methods for constructing various parts of the product, allowing creation of different concrete builders to produce different variants of the product.

3. ConcreteBuilder (Concrete Builder)

Implements the Builder interface, providing concrete implementations for constructing each part of the product. Each ConcreteBuilder is tailored for specific product variants and is responsible for tracking the state of the product being constructed.

4. Director

Responsible for managing the construction process of complex objects. It works with the Builder to provide high-level construction process control, but doesn’t need to know the specific construction details of each part of the object.

5. Client

The code that initiates the complex object construction process. It creates Builder objects and passes them to the Director, retrieving the final product from the Builder after construction is complete.

Applying to Bubble Tea System

Let’s apply the Builder Pattern to the bubble tea making system:

Through this design, we get a completely new and elegant solution (Resulting Context) that can flexibly handle complex bubble tea construction requirements.

Object-Oriented Programming (OOP)

Implementing Builder Pattern

Now let’s convert the Builder Pattern design into code implementation. Through step-by-step construction processes, we can elegantly handle the creation of complex beverages.

Product Interface Definition

First, define the abstract interface for beverages:

interface Beverage {
    var hasPearls: Boolean
    var hasCoconutJelly: Boolean
    var hasRedBeans: Boolean
    var hasGrassJelly: Boolean
    var hasPudding: Boolean
}

Concrete Product Classes

Next, implement concrete beverage products:

Bubble Tea:

data class BubbleTea(override var hasPearls: Boolean,
                     override var hasCoconutJelly: Boolean = false,
                     override var hasRedBeans: Boolean = false,
                     override var hasGrassJelly: Boolean = false,
                     override var hasPudding: Boolean = false
): Beverage {
}

Grass Jelly Pudding Tea:

data class GrassJellyPuddingTea(override var hasPearls: Boolean = false,
                     override var hasCoconutJelly: Boolean = false,
                     override var hasRedBeans: Boolean = false,
                     override var hasGrassJelly: Boolean,
                     override var hasPudding: Boolean
): Beverage {
}

Abstract Builder Interface

Define the common interface for builders:

interface Builder {
    fun addPearls(): Builder
    fun addPudding(): Builder
    fun addGrassJelly(): Builder

    fun build(): Beverage
}

Concrete Builder Implementations

Bubble Tea Builder:

class BubbleTeaBuilder: Builder {
    private var bubbleTea = BubbleTea(false)

    override fun addPearls(): BubbleTeaBuilder {
        bubbleTea.hasPearls = true
        return this
    }

    override fun addPudding(): Builder {
        return this  // Bubble tea doesn't support pudding, return directly
    }

    override fun addGrassJelly(): Builder {
        return this  // Bubble tea doesn't support grass jelly, return directly
    }

    override fun build(): BubbleTea {
        return bubbleTea
    }
}

Grass Jelly Pudding Tea Builder:

class GrassJellyPuddingTeaBuilder: Builder {
    private var grassJellyPuddingTea = GrassJellyPuddingTea(
        false,
        hasCoconutJelly = false,
        hasRedBeans = false,
        hasGrassJelly = false,
        hasPudding = false
    )

    override fun addPearls(): Builder {
        return this  // Grass jelly pudding tea doesn't support pearls, return directly
    }

    override fun addGrassJelly(): GrassJellyPuddingTeaBuilder {
        grassJellyPuddingTea.hasGrassJelly = true
        return this
    }

    override fun addPudding(): GrassJellyPuddingTeaBuilder {
        grassJellyPuddingTea.hasPudding = true
        return this
    }

    override fun build(): GrassJellyPuddingTea {
        return grassJellyPuddingTea
    }
}

Director Class

Responsible for controlling the high-level logic of the construction process:

class BeverageMaker(val builder: Builder) {
    fun makeBubbleTea(): Beverage {
        return builder.addPearls().build()
    }

    fun makeGrassJellyPuddingTea(): Beverage {
        return builder.addGrassJelly().addPudding().build()
    }
}

Client Usage Example

Finally, the actual usage code:

fun main() {
    // Make bubble tea
    val bubbleTeaBuilder = BubbleTeaBuilder()
    val bubbleTeaBeverageMaker = BeverageMaker(bubbleTeaBuilder)
    val bubbleTea = bubbleTeaBeverageMaker.makeBubbleTea()
    println(bubbleTea)

    // Make grass jelly pudding tea
    val grassJellyPuddingTeaBuilder = GrassJellyPuddingTeaBuilder()
    val grassJellyPuddingTeaBeverageMaker = BeverageMaker(grassJellyPuddingTeaBuilder)
    val grassJellyPuddingTea = grassJellyPuddingTeaBeverageMaker.makeGrassJellyPuddingTea()
    println(grassJellyPuddingTea)
}

Pattern Advantages Demonstrated

Through the Builder Pattern, we successfully solved the problem of complex object construction:

Fluent Construction Process: Able to clearly make bubble tea step by step, with each step’s intention very clear.

Avoiding Telescoping Constructor: No longer need huge parameter lists or numerous constructors.

Flexibility: Can easily support new beverage types and topping combinations.

Readability: Code intention is clear, easy to understand and maintain.

Builder Pattern Application Scenarios

Applicable Timing Judgment

The Builder Pattern is particularly useful in the following situations:

Complex Object Construction: When objects contain multiple optional properties, and these property combinations are very complex.

Step-by-Step Construction Requirements: When object construction processes require multiple steps, and these steps have specific orders or logic.

Avoiding Telescoping Constructor: When constructor parameters are too many, leading to code that’s difficult to maintain and understand.

Different Representation Requirements: When the same construction process needs to create objects with different representations.

Real-World Examples

SQL Query Builder:

val query = QueryBuilder()
    .select("name", "email")
    .from("users")
    .where("age > 18")
    .orderBy("name")
    .build()

HTTP Request Builder:

val request = HttpRequestBuilder()
    .url("https://api.example.com/users")
    .method(GET)
    .header("Authorization", "Bearer token")
    .timeout(30000)
    .build()

Evolution of Creational Patterns

Progression from Simple to Complex

Through this series of learning, we’ve seen the evolution of creational patterns:

  1. Simple Factory Pattern: Solves basic object creation problems
  2. Factory Method Pattern: Increases extensibility, supports different product types
  3. Abstract Factory Pattern: Handles product family creation, supports two-dimensional relationships
  4. Builder Pattern: Focuses on step-by-step construction processes of complex objects

Each pattern targets specific problem scenarios, and choosing the appropriate pattern is an important skill in software design.

Summary

Pattern Value

The Builder Pattern provides us with an elegant way to handle complex object construction problems. It separates construction logic from representation, making code clearer and more flexible.

Key Benefits

  • Solves Telescoping Constructor: Avoids problems with overly long parameter lists
  • Improves Readability: Construction process is clear and easy to understand
  • Enhances Flexibility: Easy to support new product variants
  • Separates Concerns: Construction logic is separated from product representation

Design Principles Embodied

The Builder Pattern embodies multiple important design principles:

  • Single Responsibility Principle: Each builder is only responsible for specific type of product construction
  • Open Closed Principle: Open to extension, easy to add new builders
  • Program to Interfaces: Depend on abstract builder interfaces rather than concrete implementations
  • Encapsulate What Varies: Encapsulate changing construction logic in different builders

Relationship with Other Creational Patterns

  • vs Abstract Factory: Builder focuses on “how to construct,” Abstract Factory focuses on “what to construct”
  • vs Factory Method: Builder supports step-by-step construction, Factory Method typically creates at once
  • Complementarity: Can be used together, for example using Factory Method to create builder instances

Future Outlook

Learning design patterns is an important part of improving software design capabilities. Mastering these patterns can not only solve specific technical problems but also cultivate good design thinking, laying a solid foundation for developing complex software systems.

Note: If you have any suggestions, questions, or different ideas, feel free to leave a comment or send me an email. We can discuss and learn together




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • Claude Code 使用技巧與最佳實踐 - Tips and Best Practices
  • 🤖 AI Agent Series (Part 1): Understanding the Core Interaction Logic of LLM, RAG, and MCP
  • 💡 Managing Multiple GitHub Accounts on One Computer: The Simplest SSH Configuration Method
  • 🚀 How to Use Excalidraw AI to Quickly Generate Professional Diagrams and Boost Work Efficiency!
  • Complete macOS Development Environment Setup Guide: Mobile Development Toolchain Configuration Tutorial