StackPractices
intermediate Por Mathias Paulenko

Patrón Builder

Construye objetos complejos paso a paso. Patrón de diseño creacional para construcción de objetos legible y configurable.

Temas: design

Visión general

El Patrón Builder es un patrón de diseño creacional que te permite construir objetos complejos paso a paso. Separa la construcción de un objeto de su representación, permitiendo que el mismo proceso de construcción cree diferentes representaciones.

Brilla cuando un objeto tiene muchos parámetros opcionales, componentes anidados, o cuando quieres una API directa y legible para la creación de objetos.

Cuándo usarlo

Usa el Patrón Builder cuando:

  • Un objeto tiene muchos parámetros de configuración opcionales o anidados
  • Quieres forzar una secuencia específica de construcción
  • El constructor tendría demasiados parámetros (problema del constructor telescópico)
  • Necesitas diferentes configuraciones del mismo tipo de objeto
  • Quieres un objeto inmutable construido desde un builder mutable

Solución

Python

class Pizza:
    def __init__(self, size, cheese=False, pepperoni=False, mushrooms=False):
        self.size = size
        self.cheese = cheese
        self.pepperoni = pepperoni
        self.mushrooms = mushrooms

    def __str__(self):
        toppings = []
        if self.cheese: toppings.append("cheese")
        if self.pepperoni: toppings.append("pepperoni")
        if self.mushrooms: toppings.append("mushrooms")
        return f"Pizza({self.size}, {', '.join(toppings) or 'plain'})"

class PizzaBuilder:
    def __init__(self, size):
        self.size = size
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False

    def add_cheese(self):
        self.cheese = True
        return self

    def add_pepperoni(self):
        self.pepperoni = True
        return self

    def build(self):
        return Pizza(self.size, self.cheese, self.pepperoni, self.mushrooms)

# Uso
pizza = PizzaBuilder("large").add_cheese().add_pepperoni().build()
print(pizza)  # Pizza(large, cheese, pepperoni)

JavaScript

class Pizza {
  constructor(size, cheese, pepperoni, mushrooms) {
    this.size = size;
    this.cheese = cheese;
    this.pepperoni = pepperoni;
    this.mushrooms = mushrooms;
  }

  toString() {
    const toppings = [
      this.cheese && "cheese",
      this.pepperoni && "pepperoni",
      this.mushrooms && "mushrooms",
    ].filter(Boolean);
    return `Pizza(${this.size}, ${toppings.join(", ") || "plain"})`;
  }
}

class PizzaBuilder {
  constructor(size) {
    this.size = size;
    this.cheese = false;
    this.pepperoni = false;
    this.mushrooms = false;
  }

  addCheese() { this.cheese = true; return this; }
  addPepperoni() { this.pepperoni = true; return this; }
  addMushrooms() { this.mushrooms = true; return this; }
  build() { return new Pizza(this.size, this.cheese, this.pepperoni, this.mushrooms); }
}

// Uso
const pizza = new PizzaBuilder("large").addCheese().addPepperoni().build();
console.log(pizza.toString()); // Pizza(large, cheese, pepperoni)

Java

public class Pizza {
    private final String size;
    private final boolean cheese;
    private final boolean pepperoni;
    private final boolean mushrooms;

    private Pizza(Builder builder) {
        this.size = builder.size;
        this.cheese = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.mushrooms = builder.mushrooms;
    }

    public static class Builder {
        private final String size;
        private boolean cheese = false;
        private boolean pepperoni = false;
        private boolean mushrooms = false;

        public Builder(String size) { this.size = size; }
        public Builder cheese() { this.cheese = true; return this; }
        public Builder pepperoni() { this.pepperoni = true; return this; }
        public Builder mushrooms() { this.mushrooms = true; return this; }
        public Pizza build() { return new Pizza(this); }
    }

    @Override
    public String toString() {
        return "Pizza(" + size + ", cheese=" + cheese + ", pepperoni=" + pepperoni + ")";
    }
}

// Uso
Pizza pizza = new Pizza.Builder("large").cheese().pepperoni().build();
System.out.println(pizza);

Explicación

El Patrón Builder separa el ensamblaje del objeto en dos partes:

  • Builder: Acumula estado de configuración y sabe cómo construir el objeto final
  • Producto (Pizza): El objeto inmutable o completamente configurado retornado por build()

Retornando self (o this) de cada método de configuración, creas una interfaz encadenable que se lee como una oración. Esto elimina constructores con docenas de parámetros.

Variantes

VarianteCaso de usoCompromiso
Fluent BuilderConstrucción legible paso a pasoRequiere estado mutable del builder
Director + BuilderMúltiples secuencias de construcciónMás clases, pero recetas reutilizables
Static Factory BuilderPatrón Class.Builder() de JavaAPI limpia, pero acoplado al producto

Lo que funciona

  • Retorna self de cada método de paso para habilitar encadenamiento de métodos
  • Haz el producto inmutable después de que se llama build()
  • Valida en build(), no en pasos individuales, para contexto completo de errores
  • Usa un Director cuando tienes configuraciones preestablecidas comunes (ej. pizzaDirector.makeMargherita())
  • Documenta pasos requeridos vs opcionales para que los llamadores sepan la configuración mínima válida

Errores comunes

  • Productos mutables: Permitir modificaciones después de build() anula el propósito
  • Validación faltante: Construir un objeto inválido porque se saltó la validación
  • Builders excesivamente complejos: Un builder para un objeto simple con 2 campos es excesivo
  • Fuga de estado: Reusar una instancia de builder después de build() sin resetear estado
  • Olvidar retornar self: Romper el encadenamiento retornando None/void

Técnicas avanzadas

Builder con validación y defaults

Añade validación en el método build() y proporciona defaults sensatos:

# Python: Builder con validación y defaults
class PizzaBuilder:
    def __init__(self, size="medium"):
        self.size = size
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False
        self.sauce = "tomato"  # Valor por defecto

    def add_cheese(self):
        self.cheese = True
        return self

    def add_pepperoni(self):
        self.pepperoni = True
        return self

    def set_sauce(self, sauce):
        valid_sauces = ["tomato", "bbq", "pesto"]
        if sauce not in valid_sauces:
            raise ValueError(f"Salsa inválida: {sauce}. Debe ser una de {valid_sauces}")
        self.sauce = sauce
        return self

    def build(self):
        if not self.size:
            raise ValueError("El tamaño es requerido")
        if self.size not in ["small", "medium", "large"]:
            raise ValueError(f"Tamaño inválido: {self.size}")
        return Pizza(self.size, self.cheese, self.pepperoni, self.mushrooms, self.sauce)

Builder para objetos anidados

Maneja grafos de objetos complejos con builders anidados:

// Java: Builder para objetos anidados
public class House {
    private final String address;
    private final Kitchen kitchen;
    private final List<Room> rooms;

    private House(Builder builder) {
        this.address = builder.address;
        this.kitchen = builder.kitchen;
        this.rooms = builder.rooms;
    }

    public static class Builder {
        private String address;
        private Kitchen kitchen;
        private List<Room> rooms = new ArrayList<>();

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public Builder kitchen(Kitchen.Builder kitchenBuilder) {
            this.kitchen = kitchenBuilder.build();
            return this;
        }

        public Builder addRoom(Room.Builder roomBuilder) {
            this.rooms.add(roomBuilder.build());
            return this;
        }

        public House build() {
            return new House(this);
        }
    }
}

// Uso
House house = new House.Builder()
    .address("123 Main St")
    .kitchen(new Kitchen.Builder().size("large").build())
    .addRoom(new Room.Builder().type("bedroom").size("medium").build())
    .addRoom(new Room.Builder().type("bathroom").size("small").build())
    .build();

Builder con métodos de copia

Soporta crear un builder desde un objeto existente para modificación:

// JavaScript: Builder con métodos de copia
class Pizza {
  constructor(size, cheese, pepperoni, mushrooms, sauce) {
    this.size = size;
    this.cheese = cheese;
    this.pepperoni = pepperoni;
    this.mushrooms = mushrooms;
    this.sauce = sauce;
  }

  static fromBuilder(builder) {
    return new Pizza(
      builder.size,
      builder.cheese,
      builder.pepperoni,
      builder.mushrooms,
      builder.sauce
    );
  }

  toBuilder() {
    return new PizzaBuilder(this.size)
      .addCheese(this.cheese)
      .addPepperoni(this.pepperoni)
      .addMushrooms(this.mushrooms)
      .setSauce(this.sauce);
  }
}

// Uso: Modificar pizza existente
const originalPizza = new PizzaBuilder("large").addCheese().build();
const modifiedPizza = originalPizza.toBuilder().addPepperoni().build();

Builder con encadenamiento de métodos para construcción condicional

Soporta patrones de construcción condicional:

# Python: Construcción condicional
class QueryBuilder:
    def __init__(self):
        self.conditions = []
        self.joins = []
        self.order_by = None
        self.limit = None

    def where(self, condition):
        self.conditions.append(condition)
        return self

    def join(self, table, on_clause):
        self.joins.append((table, on_clause))
        return self

    def order_by(self, field, direction="ASC"):
        self.order_by = (field, direction)
        return self

    def limit(self, count):
        self.limit = count
        return self

    def build(self):
        query = "SELECT * FROM items"
        if self.joins:
            for table, on_clause in self.joins:
                query += f" JOIN {table} ON {on_clause}"
        if self.conditions:
            query += " WHERE " + " AND ".join(self.conditions)
        if self.order_by:
            query += f" ORDER BY {self.order_by[0]} {self.order_by[1]}"
        if self.limit:
            query += f" LIMIT {self.limit}"
        return query

# Uso con lógica condicional
builder = QueryBuilder()
builder.join("users", "items.user_id = users.id")

if include_active_only:
    builder.where("users.active = true")

if sort_by_date:
    builder.order_by("created_at", "DESC")

if max_results:
    builder.limit(max_results)

query = builder.build()

Builder con construcción paralela

Soporta construir múltiples objetos desde configuración compartida:

// Java: Builder con construcción paralela
public class ReportBuilder {
    private String title;
    private String author;
    private String date;
    private List<Section> sections = new ArrayList<>();

    public ReportBuilder title(String title) {
        this.title = title;
        return this;
    }

    public ReportBuilder author(String author) {
        this.author = author;
        return this;
    }

    public ReportBuilder date(String date) {
        this.date = date;
        return this;
    }

    public ReportBuilder addSection(Section section) {
        this.sections.add(section);
        return this;
    }

    public PDFReport buildPDF() {
        return new PDFReport(title, author, date, sections);
    }

    public HTMLReport buildHTML() {
        return new HTMLReport(title, author, date, sections);
    }

    public MarkdownReport buildMarkdown() {
        return new MarkdownReport(title, author, date, sections);
    }
}

// Uso: Construir múltiples formatos desde la misma configuración
ReportBuilder builder = new ReportBuilder()
    .title("Q4 Sales Report")
    .author("John Doe")
    .date("2026-01-15")
    .addSection(new Section("Executive Summary", "..."))
    .addSection(new Section("Data Analysis", "..."));

PDFReport pdf = builder.buildPDF();
HTMLReport html = builder.buildHTML();
MarkdownReport markdown = builder.buildMarkdown();

Mejores prácticas

  1. Valida solo en build(). Difiera la validación hasta el paso final para proporcionar contexto completo de errores con todos los problemas de configuración.

  2. Haz el producto inmutable. Una vez que build() retorna el objeto, no debería ser modificable. Esto previene estado inconsistente.

  3. Documenta parámetros requeridos. Distingue claramente entre pasos de configuración requeridos y opcionales en tu documentación.

  4. Usa nombres de métodos descriptivos. Los nombres de métodos deberían indicar claramente qué configuran (ej. withTimeout() vs setTimeout()).

  5. Proporciona defaults sensatos. Los valores por defecto reducen el número de llamadas de método requeridas para casos de uso comunes.

  6. Considera un constructor de copia. Permite crear un builder desde un objeto existente para soportar patrones de modificación.

  7. Maneja null gracefulmente. Decide si permitir valores null o lanzar excepciones, y sé consistente.

  8. Thread-safety para builders compartidos. Si los builders se reusan entre threads, asegúrate que sean thread-safe o no compartidos.

  9. Soporta serialización. Considera añadir métodos para serializar/deserializar el estado del builder para persistencia.

  10. Mantén los builders enfocados. Un builder debería construir un tipo de objeto. No añadas lógica de construcción no relacionada.

Preguntas frecuentes

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.