Skip to main content

Definition

“Builder Pattern constructs a complex object step by step, and the final step will return the object. The process of constructing an object should be generic so that it can be used to create different representations of the same object”
builder

Explanation

You create a Builder interface with methods for each part of the product, and a Director that uses a builder to construct the final object. Concrete builders implement the steps to build different representations. Clients use the director and builder to assemble the product incrementally. This separates object construction from representation.

Code

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


# ===== Products (can be totally different types) =====

@dataclass
class Product1:
    name: str = "Product1"
    features: list[str] = field(default_factory=list)

    def __str__(self) -> str:
        return f"{self.name}({', '.join(self.features) or 'no features'})"


@dataclass
class Product2:
    name: str = "Product2"
    parts: list[str] = field(default_factory=list)

    def __str__(self) -> str:
        return f"{self.name}({', '.join(self.parts) or 'no parts'})"


# ===== Builder interface (per the UML) =====

class Builder(ABC):
    @abstractmethod
    def reset(self) -> None: ...
    @abstractmethod
    def buildStepA(self) -> None: ...
    @abstractmethod
    def buildStepB(self) -> None: ...
    @abstractmethod
    def buildStepZ(self) -> None: ...
    @abstractmethod
    def getResult(self):
        """Return the product and (optionally) prepare for a new build."""


# ===== Concrete builders (each yields a different product) =====

class ConcreteBuilder1(Builder):
    def __init__(self) -> None:
        self._product: Product1 | None = None
        self.reset()

    def reset(self) -> None:
        self._product = Product1()

    def buildStepA(self) -> None:
        self._product.features.append("FeatureA1")

    def buildStepB(self) -> None:
        self._product.features.append("FeatureB1")

    def buildStepZ(self) -> None:
        self._product.features.append("FeatureZ1")

    def getResult(self) -> Product1:
        # Typical builder behavior: hand off the current product and reset
        product = self._product
        self.reset()
        return product


class ConcreteBuilder2(Builder):
    def __init__(self) -> None:
        self._product: Product2 | None = None
        self.reset()

    def reset(self) -> None:
        self._product = Product2()

    def buildStepA(self) -> None:
        self._product.parts.append("PartA2")

    def buildStepB(self) -> None:
        self._product.parts.append("PartB2")

    def buildStepZ(self) -> None:
        self._product.parts.append("PartZ2")

    def getResult(self) -> Product2:
        product = self._product
        self.reset()
        return product


# ===== Director (defines build order / reusable recipes) =====

class Director:
    def __init__(self, builder: Builder) -> None:
        self.builder = builder

    def changeBuilder(self, builder: Builder) -> None:
        self.builder = builder

    def make(self, type: str = "simple") -> None:
        """
        Reusable 'recipes' that decide which steps to call and in what order.
        The Director does NOT return the product; the client asks the builder.
        """
        self.builder.reset()
        if type == "simple":
            self.builder.buildStepA()
        elif type == "standard":
            self.builder.buildStepA()
            self.builder.buildStepB()
        elif type == "deluxe":
            self.builder.buildStepB()
            self.builder.buildStepZ()
        elif type == "ultimate":
            self.builder.buildStepA()
            self.builder.buildStepB()
            self.builder.buildStepZ()
        else:
            raise ValueError(f"Unknown build type: {type}")


# ===== Client code (matches the call sequence in the diagram) =====

if __name__ == "__main__":
    # b = new ConcreteBuilder1()
    b = ConcreteBuilder1()
    # d = new Director(b)
    d = Director(b)
    # d.make()
    d.make("ultimate")
    # Product1 p = b.getResult()
    p1 = b.getResult()
    print(p1)  # -> Product1(FeatureA1, FeatureB1, FeatureZ1)

    # Switch builder at runtime and reuse the same Director "recipes"
    d.changeBuilder(ConcreteBuilder2())
    d.make("simple")
    p2 = d.builder.getResult()  # or keep a reference to the builder instance
    print(p2)  # -> Product2(PartA2)


Analogy

Building a house or assembling a custom computer: the builder can produce different models (modern vs medieval house) by changing how each part is built, while the construction steps (floor, walls, roof) are the same. Or a meal order at a fast-food restaurant: a standard meal builder picks burger, drink, sides; you can create different meals by choosing different implementations of builder (veg meal vs chicken meal).

Interview Insights

Common uses: When an object has many optional parts or complex construction (e.g. creating documents with optional sections, building HTML with various tags, configuring complex objects in tests). Often used for toString() or parsing, or in Fluent APIs.
Advantages: Encapsulates construction code; improves readability. Supports step-by-step construction and validation. Isolates the creation of different representations of an object.
Disadvantages: More classes/code overhead. Can be overkill if object construction is simple.\