Patrón Builder
Construye objetos complejos paso a paso. Patrón de diseño creacional para construcción de objetos legible y configurable.
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 porbuild()
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
| Variante | Caso de uso | Compromiso |
|---|---|---|
| Fluent Builder | Construcción legible paso a paso | Requiere estado mutable del builder |
| Director + Builder | Múltiples secuencias de construcción | Más clases, pero recetas reutilizables |
| Static Factory Builder | Patrón Class.Builder() de Java | API limpia, pero acoplado al producto |
Lo que funciona
- Retorna
selfde 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 retornandoNone/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
-
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. -
Haz el producto inmutable. Una vez que
build()retorna el objeto, no debería ser modificable. Esto previene estado inconsistente. -
Documenta parámetros requeridos. Distingue claramente entre pasos de configuración requeridos y opcionales en tu documentación.
-
Usa nombres de métodos descriptivos. Los nombres de métodos deberían indicar claramente qué configuran (ej.
withTimeout()vssetTimeout()). -
Proporciona defaults sensatos. Los valores por defecto reducen el número de llamadas de método requeridas para casos de uso comunes.
-
Considera un constructor de copia. Permite crear un builder desde un objeto existente para soportar patrones de modificación.
-
Maneja null gracefulmente. Decide si permitir valores null o lanzar excepciones, y sé consistente.
-
Thread-safety para builders compartidos. Si los builders se reusan entre threads, asegúrate que sean thread-safe o no compartidos.
-
Soporta serialización. Considera añadir métodos para serializar/deserializar el estado del builder para persistencia.
-
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.
Recursos Relacionados
Patron Factory
Crea objetos sin especificar la clase exacta a instanciar. Un patrón de diseño creacional para la creación flexible de objetos.
PatternPatrón Singleton
Garantiza que una clase tenga una única instancia y proporciona un acceso global a ella. Patrón de diseño creacional para controlar la creación de objetos.
PatternPatrón Decorator
Añade nueva funcionalidad a objetos dinámicamente envolviéndolos. Patrón de diseño estructural para extensión flexible de comportamiento.
PatternPatrón Abstract Factory
Crea familias de objetos relacionados sin especificar sus clases concretas. Patrón de diseño creacional para familias de objetos consistentes.
PatternPrototype Pattern para Clonacion de Objetos y Plantillas
Crea nuevos objetos copiando existentes, permitiendo plantillas pre-configuradas y evitando explosion de subclases cuando la creacion de objetos es costosa
PatternPatrón Prototype
Crea nuevos objetos copiando los existentes. Un patrón de diseño creacional para clonación y duplicación de objetos.