Design Pattern 8: Builder Pattern - Step-by-Step Construction of Complex Objects for Flexible Configuration

Download the complete Design Pattern series code from the design_pattern repo.

Introduction: The Challenge of Complex Object Construction

The Builder Pattern is a creational design pattern that allows you to construct complex objects step by step. This pattern is particularly useful when you need to create objects with many optional parameters or when the construction process involves multiple steps.

Real-World Applications

The Builder Pattern is widely used in:

  • Configuration Objects: Building complex configuration with optional parameters
  • Database Queries: Constructing SQL queries with dynamic conditions
  • UI Components: Building complex UI elements with multiple properties
  • API Requests: Creating HTTP requests with various headers and parameters
  • Game Development: Constructing game objects with different attributes

Problem Statement: Beverage Machine Configuration

We need to design an automated beverage machine that can create various bubble tea combinations. The machine should support multiple toppings and ingredients to attract a wide customer base.

Available Toppings:

  • Pearls (珍珠)
  • Coconut Jelly (椰果)
  • Red Beans (紅豆)
  • Grass Jelly (仙草凍)
  • Pudding (布丁)

Object-Oriented Analysis (OOA)

Let’s analyze the requirements and design our initial solution:

However, this approach has a problem: if we only want to add red beans and pudding, we must pass false or null for other unused parameters. This becomes difficult to maintain and reduces readability as the number of parameters increases.

A smarter approach might be to use multiple constructors to avoid passing unnecessary parameters:

Identifying Design Forces

As the number of parameters increases, we need more constructors, making the class difficult to maintain and the instantiation process complex. This phenomenon is known as the telescoping constructor problem.

Telescoping Constructor: When a class has multiple constructors with different parameter counts, leading to difficult maintenance and usage.

Applying Builder Pattern Solution

The Builder Pattern provides an elegant solution by separating object construction from its representation.

Builder Pattern UML Structure

Key Components:

  • Product: The complex object being built
  • Builder: Interface defining construction steps
  • ConcreteBuilder: Implements specific construction logic
  • Director: Manages the construction process
  • Client: Initiates the building process

Applied to Beverage System

Implementation: Object-Oriented Programming (OOP)

Product Interface

interface Beverage {
    var hasPearls: Boolean
    var hasCoconutJelly: Boolean
    var hasRedBeans: Boolean
    var hasGrassJelly: Boolean
    var hasPudding: Boolean
    
    fun getDescription(): String
    fun getPrice(): Double
}

Concrete Products

data class BubbleTea(
    override var hasPearls: Boolean = false,
    override var hasCoconutJelly: Boolean = false,
    override var hasRedBeans: Boolean = false,
    override var hasGrassJelly: Boolean = false,
    override var hasPudding: Boolean = false
): Beverage {
    override fun getDescription(): String {
        val toppings = mutableListOf<String>()
        if (hasPearls) toppings.add("Pearls")
        if (hasCoconutJelly) toppings.add("Coconut Jelly")
        if (hasRedBeans) toppings.add("Red Beans")
        if (hasGrassJelly) toppings.add("Grass Jelly")
        if (hasPudding) toppings.add("Pudding")
        
        return "Bubble Tea with: ${toppings.joinToString(", ")}"
    }
    
    override fun getPrice(): Double {
        var basePrice = 5.0
        if (hasPearls) basePrice += 1.0
        if (hasCoconutJelly) basePrice += 0.5
        if (hasRedBeans) basePrice += 0.8
        if (hasGrassJelly) basePrice += 0.6
        if (hasPudding) basePrice += 1.2
        return basePrice
    }
}

data class GrassJellyPuddingTea(
    override var hasPearls: Boolean = false,
    override var hasCoconutJelly: Boolean = false,
    override var hasRedBeans: Boolean = false,
    override var hasGrassJelly: Boolean = false,
    override var hasPudding: Boolean = false
): Beverage {
    override fun getDescription(): String {
        val toppings = mutableListOf<String>()
        if (hasPearls) toppings.add("Pearls")
        if (hasCoconutJelly) toppings.add("Coconut Jelly")
        if (hasRedBeans) toppings.add("Red Beans")
        if (hasGrassJelly) toppings.add("Grass Jelly")
        if (hasPudding) toppings.add("Pudding")
        
        return "Grass Jelly Pudding Tea with: ${toppings.joinToString(", ")}"
    }
    
    override fun getPrice(): Double {
        var basePrice = 6.0
        if (hasPearls) basePrice += 1.0
        if (hasCoconutJelly) basePrice += 0.5
        if (hasRedBeans) basePrice += 0.8
        if (hasGrassJelly) basePrice += 0.6
        if (hasPudding) basePrice += 1.2
        return basePrice
    }
}

Builder Interface

interface BeverageBuilder {
    fun addPearls(): BeverageBuilder
    fun addCoconutJelly(): BeverageBuilder
    fun addRedBeans(): BeverageBuilder
    fun addGrassJelly(): BeverageBuilder
    fun addPudding(): BeverageBuilder
    fun reset(): BeverageBuilder
    fun build(): Beverage
}

Concrete Builders

class BubbleTeaBuilder: BeverageBuilder {
    private var bubbleTea = BubbleTea()

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

    override fun addCoconutJelly(): BubbleTeaBuilder {
        bubbleTea.hasCoconutJelly = true
        return this
    }

    override fun addRedBeans(): BubbleTeaBuilder {
        bubbleTea.hasRedBeans = true
        return this
    }

    override fun addGrassJelly(): BubbleTeaBuilder {
        bubbleTea.hasGrassJelly = true
        return this
    }

    override fun addPudding(): BubbleTeaBuilder {
        bubbleTea.hasPudding = true
        return this
    }

    override fun reset(): BubbleTeaBuilder {
        bubbleTea = BubbleTea()
        return this
    }

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

class GrassJellyPuddingTeaBuilder: BeverageBuilder {
    private var grassJellyPuddingTea = GrassJellyPuddingTea()

    override fun addPearls(): GrassJellyPuddingTeaBuilder {
        grassJellyPuddingTea.hasPearls = true
        return this
    }

    override fun addCoconutJelly(): GrassJellyPuddingTeaBuilder {
        grassJellyPuddingTea.hasCoconutJelly = true
        return this
    }

    override fun addRedBeans(): GrassJellyPuddingTeaBuilder {
        grassJellyPuddingTea.hasRedBeans = true
        return this
    }

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

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

    override fun reset(): GrassJellyPuddingTeaBuilder {
        grassJellyPuddingTea = GrassJellyPuddingTea()
        return this
    }

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

Director (Optional)

class BeverageMaker(private val builder: BeverageBuilder) {
    fun makeClassicBubbleTea(): Beverage {
        return builder.reset()
            .addPearls()
            .build()
    }

    fun makeDeluxeBubbleTea(): Beverage {
        return builder.reset()
            .addPearls()
            .addCoconutJelly()
            .addPudding()
            .build()
    }

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

    fun makeCustomBeverage(configuration: (BeverageBuilder) -> BeverageBuilder): Beverage {
        return configuration(builder.reset()).build()
    }
}

Client Usage

fun main() {
    // Using Builder directly
    val bubbleTeaBuilder = BubbleTeaBuilder()
    val classicBubbleTea = bubbleTeaBuilder
        .addPearls()
        .build()
    
    println("Classic Bubble Tea: ${classicBubbleTea.getDescription()}")
    println("Price: $${classicBubbleTea.getPrice()}")

    // Using Director
    val beverageMaker = BeverageMaker(BubbleTeaBuilder())
    val deluxeBubbleTea = beverageMaker.makeDeluxeBubbleTea()
    
    println("Deluxe Bubble Tea: ${deluxeBubbleTea.getDescription()}")
    println("Price: $${deluxeBubbleTea.getPrice()}")

    // Custom configuration
    val customBeverage = beverageMaker.makeCustomBeverage { builder ->
        builder.addPearls()
            .addRedBeans()
            .addPudding()
    }
    
    println("Custom Beverage: ${customBeverage.getDescription()}")
    println("Price: $${customBeverage.getPrice()}")
}

Advanced Implementation: Fluent Builder with Validation

class AdvancedBeverageBuilder {
    private var hasPearls = false
    private var hasCoconutJelly = false
    private var hasRedBeans = false
    private var hasGrassJelly = false
    private var hasPudding = false
    private var sweetness: SweetnessLevel = SweetnessLevel.NORMAL
    private var iceLevel: IceLevel = IceLevel.NORMAL

    fun addPearls(): AdvancedBeverageBuilder {
        hasPearls = true
        return this
    }

    fun addCoconutJelly(): AdvancedBeverageBuilder {
        hasCoconutJelly = true
        return this
    }

    fun addRedBeans(): AdvancedBeverageBuilder {
        hasRedBeans = true
        return this
    }

    fun addGrassJelly(): AdvancedBeverageBuilder {
        hasGrassJelly = true
        return this
    }

    fun addPudding(): AdvancedBeverageBuilder {
        hasPudding = true
        return this
    }

    fun setSweetness(level: SweetnessLevel): AdvancedBeverageBuilder {
        sweetness = level
        return this
    }

    fun setIceLevel(level: IceLevel): AdvancedBeverageBuilder {
        iceLevel = level
        return this
    }

    fun build(): AdvancedBeverage {
        // Validation
        if (!hasPearls && !hasCoconutJelly && !hasRedBeans && !hasGrassJelly && !hasPudding) {
            throw IllegalArgumentException("At least one topping must be selected")
        }
        
        return AdvancedBeverage(
            hasPearls, hasCoconutJelly, hasRedBeans, hasGrassJelly, hasPudding,
            sweetness, iceLevel
        )
    }
}

enum class SweetnessLevel { NO_SUGAR, LESS, NORMAL, MORE, EXTRA }
enum class IceLevel { NO_ICE, LESS, NORMAL, MORE, EXTRA }

data class AdvancedBeverage(
    val hasPearls: Boolean,
    val hasCoconutJelly: Boolean,
    val hasRedBeans: Boolean,
    val hasGrassJelly: Boolean,
    val hasPudding: Boolean,
    val sweetness: SweetnessLevel,
    val iceLevel: IceLevel
) {
    fun getDescription(): String {
        val toppings = mutableListOf<String>()
        if (hasPearls) toppings.add("Pearls")
        if (hasCoconutJelly) toppings.add("Coconut Jelly")
        if (hasRedBeans) toppings.add("Red Beans")
        if (hasGrassJelly) toppings.add("Grass Jelly")
        if (hasPudding) toppings.add("Pudding")
        
        return "Beverage with: ${toppings.joinToString(", ")}, " +
               "Sweetness: ${sweetness.name}, Ice: ${iceLevel.name}"
    }
}

Best Practices and Considerations

1. Fluent Interface Design

// Good: Fluent interface with method chaining
class FluentBuilder {
    private var property1 = ""
    private var property2 = 0
    
    fun setProperty1(value: String): FluentBuilder {
        property1 = value
        return this
    }
    
    fun setProperty2(value: Int): FluentBuilder {
        property2 = value
        return this
    }
    
    fun build(): Product {
        return Product(property1, property2)
    }
}

// Usage
val product = FluentBuilder()
    .setProperty1("value")
    .setProperty2(42)
    .build()

2. Validation and Error Handling

// Good: Validation in build method
class ValidatedBuilder {
    private var requiredField = ""
    private var optionalField = ""
    
    fun setRequiredField(value: String): ValidatedBuilder {
        requiredField = value
        return this
    }
    
    fun setOptionalField(value: String): ValidatedBuilder {
        optionalField = value
        return this
    }
    
    fun build(): Product {
        if (requiredField.isEmpty()) {
            throw IllegalStateException("Required field must be set")
        }
        return Product(requiredField, optionalField)
    }
}

3. Immutable Products

// Good: Immutable product with builder
data class ImmutableProduct(
    val name: String,
    val description: String,
    val price: Double,
    val tags: List<String>
) {
    class Builder {
        private var name = ""
        private var description = ""
        private var price = 0.0
        private var tags = mutableListOf<String>()
        
        fun name(value: String) = apply { name = value }
        fun description(value: String) = apply { description = value }
        fun price(value: Double) = apply { price = value }
        fun addTag(tag: String) = apply { tags.add(tag) }
        
        fun build() = ImmutableProduct(name, description, price, tags.toList())
    }
}

Performance Considerations

Approach Memory Usage Performance Readability Maintainability
Telescoping Constructor Low High Low Low
Builder Pattern Medium Medium High High
Setter Methods Low High Medium Medium
Factory Method Low High Medium Medium
  • Factory Method: Creates objects without specifying exact classes
  • Abstract Factory: Creates families of related objects
  • Prototype: Creates new objects by cloning existing ones
  • Singleton: Ensures only one instance exists

Conclusion

The Builder Pattern provides a powerful way to construct complex objects step by step. Key benefits include:

  • Improved Readability: Clear, fluent interface for object construction
  • Flexible Configuration: Easy to handle optional parameters
  • Validation Support: Built-in validation during construction
  • Maintainability: Easy to add new construction steps

This pattern is essential for building complex objects with many optional parameters or when the construction process involves multiple steps.




    Enjoy Reading This Article?

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

  • How to Use Multiple GitHub Accounts on One Computer: Complete SSH Setup Guide
  • Excalidraw AI: Create Professional Diagrams with Text Commands - Complete Guide
  • Complete macOS Development Environment Setup Guide for 2024
  • Design Pattern 28: Interpreter Pattern - Complete Guide with Examples
  • Design Pattern 27: Visitor Pattern - Complete Guide with Real-World IoT Examples