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 fluida 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 el estado de configuración y sabe cómo construir el objeto final
- Producto (
Pizza): El objeto inmutable o completamente configurado que devuelvebuild()
Al devolver self (o this) desde cada método de configuración, creas una interfaz fluida 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 |
Builder vs. alternativas más simples
No todo objeto necesita un builder. Usa esta comparación para decidir:
| Enfoque | Cuándo gana | Por qué no un Builder |
|---|---|---|
| Constructor simple | Pocos parámetros, todos obligatorios | El encadenamiento añade código repetitivo innecesario |
| Constructor telescópico | Un conjunto pequeño de combinaciones fijas de opciones | Explota combinatoriamente; el orden de argumentos es propenso a errores |
| Objeto literal / spread | Bolsa de opciones plana sin invariantes | Sin validación, valores por defecto ni invariantes en la construcción |
| Método factory | Lógica de creación puntual | No compone opciones paso a paso |
Lo que funciona
- Devuelve
selfde cada método de paso para habilitar el encadenamiento de métodos - Haz el producto inmutable después de que se llama
build() - Valida en
build(), no en pasos individuales, para tener contexto completo de errores - Usa un Director cuando tienes configuraciones preestablecidas comunes (ej.
pizzaDirector.makeMargherita()) - Documenta pasos obligatorios vs opcionales para que quien llame sepa 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 omitió la validación
- Builders excesivamente complejos: Un builder para un objeto simple con 2 campos es excesivo
- Fuga de estado: Reutilizar una instancia de builder después de
build()sin reiniciar el estado - Olvidar devolver
self: Romper la cadena fluida devolviendoNone/void
Técnicas avanzadas
Builder con validación y valores por defecto
Añade validación en el método build() y proporciona valores por defecto razonables:
class Pizza:
def __init__(self, size, cheese=False, pepperoni=False, mushrooms=False, sauce="tomato"):
self.size = size
self.cheese = cheese
self.pepperoni = pepperoni
self.mushrooms = mushrooms
self.sauce = sauce
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 obligatorio")
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:
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
Permite crear un builder desde un objeto existente para construir copias modificadas:
class Pizza {
constructor(size, cheese, pepperoni, mushrooms) {
this.size = size;
this.cheese = cheese;
this.pepperoni = pepperoni;
this.mushrooms = mushrooms;
}
toBuilder() {
const builder = new PizzaBuilder(this.size);
if (this.cheese) builder.addCheese();
if (this.pepperoni) builder.addPepperoni();
if (this.mushrooms) builder.addMushrooms();
return builder;
}
}
// Uso: modificar una pizza existente sin reescribir su configuración
const originalPizza = new PizzaBuilder("large").addCheese().build();
const modifiedPizza = originalPizza.toBuilder().addPepperoni().build();
Construcción condicional
Los builders combinan bien con lógica condicional que aplica opciones solo cuando hacen falta. Mantén el estado acumulado en campos privados para que los nombres de atributos nunca colisionen con los métodos:
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()
Construcción de múltiples tipos de producto
Un builder puede compartir configuración entre varios productos relacionados:
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();
Construcción de objetos de configuración
Los objetos de configuración son uno de los usos reales más comunes del patrón Builder. Una configuración de base de datos o de cliente HTTP suele tener muchos campos opcionales con valores por defecto: exactamente lo que describe el problema del constructor telescópico:
class DatabaseConfig {
constructor(
public readonly host: string,
public readonly port: number,
public readonly database: string,
public readonly username: string,
public readonly password: string,
public readonly poolSize: number,
public readonly timeout: number,
public readonly ssl: boolean,
public readonly retryAttempts: number
) {}
}
class DatabaseConfigBuilder {
private host: string = 'localhost';
private port: number = 5432;
private database!: string;
private username!: string;
private password!: string;
private poolSize: number = 10;
private timeout: number = 5000;
private ssl: boolean = false;
private retryAttempts: number = 3;
setHost(host: string): this { this.host = host; return this; }
setPort(port: number): this { this.port = port; return this; }
setDatabase(database: string): this { this.database = database; return this; }
setCredentials(username: string, password: string): this {
this.username = username;
this.password = password;
return this;
}
setPoolSize(size: number): this { this.poolSize = size; return this; }
setTimeout(timeout: number): this { this.timeout = timeout; return this; }
enableSsl(): this { this.ssl = true; return this; }
setRetryAttempts(attempts: number): this { this.retryAttempts = attempts; return this; }
build(): DatabaseConfig {
if (!this.database) throw new Error('Database name is required');
if (!this.username || !this.password) {
throw new Error('Credentials are required');
}
return new DatabaseConfig(
this.host, this.port, this.database, this.username,
this.password, this.poolSize, this.timeout, this.ssl, this.retryAttempts
);
}
}
// Los presets de producción y desarrollo comparten el mismo builder
const prodConfig = new DatabaseConfigBuilder()
.setHost('db.production.com')
.setDatabase('myapp')
.setCredentials('user', 'secret')
.setPoolSize(50)
.enableSsl()
.setTimeout(10000)
.build();
const devConfig = new DatabaseConfigBuilder()
.setDatabase('myapp_dev')
.setCredentials('dev', 'dev')
.build(); // localhost:5432, pool de 10, SSL desactivado
Dos prácticas importan en los builders de configuración. Define presets por entorno (desarrollo, staging, producción) como funciones que devuelven un builder preconfigurado, de modo que cada entorno solo sobrescriba lo que difiere. Y nunca escribas secretos directamente en las llamadas del builder: cárgalos desde variables de entorno o un gestor de secretos antes de pasarlos.
Mejores prácticas
-
Usa nombres de métodos descriptivos. Los nombres deberían indicar claramente qué configuran (ej.
withTimeout()vssetTimeout()). -
Proporciona valores por defecto razonables. Reducen el número de llamadas obligatorias para los casos de uso comunes.
-
Considera un constructor de copia. Permite crear un builder desde un objeto existente para soportar patrones de modificación.
-
Maneja
nullde forma consistente. Decide si permitir valores nulos o lanzar excepciones, y sé consistente. -
Seguridad en hilos para builders compartidos. Si los builders se reutilizan entre hilos, asegúrate de que sean seguros o no se compartan.
-
Mantén los builders enfocados. Un builder debería construir un solo tipo de objeto. No añadas lógica de construcción no relacionada.
Véase también
- Builder — Refactoring.Guru: propósito, estructura y ejemplos en varios lenguajes.
- Patrón Builder — Wikipedia: definición formal del GoF e historia.
- Design Patterns (libro GoF): el catálogo original donde se publicó el patrón.
Preguntas frecuentes
¿Cuál es la diferencia entre Builder y Factory?
Factory decide qué clase instanciar; Builder ensambla un único objeto complejo paso a paso. Responden a preguntas distintas —"qué objeto crear" frente a "cómo ensamblarlo"— y a menudo aparecen juntos.
¿Debería usar un Builder para cada clase?
No. A partir de 3-4 parámetros opcionales, o cuando el orden de construcción empieza a importar, un builder se paga solo; por debajo de eso es mayormente código repetitivo.
¿Cómo se compara Builder con el patrón Prototype?
Prototype clona una instancia existente; Builder ensambla una nueva desde parámetros. Elige Prototype cuando ya tienes un objeto plantilla, Builder cuando construyes desde cero.
¿Debería usar un Builder para objetos inmutables?
Es uno de sus mejores usos: el builder carga el estado mutable durante la construcción y luego te entrega un producto inmutable.
¿Pueden los Builders usarse para objetos de configuración?
Es el caso clásico; mira el ejemplo de configuración en Técnicas avanzadas. Los ajustes opcionales, los valores por defecto y los presets por entorno salen ganando.
¿Cómo manejo parámetros opcionales en un Builder?
Dales valores por defecto en los campos o el constructor del builder, y comprueba dentro de build() que los obligatorios estén realmente establecidos.
¿Cómo manejo errores de validación en un Builder?
Recopila todos los problemas que encuentres en build() y lanza una sola excepción con la lista completa: quien llama lo arregla todo de una vez en lugar de error a error.
¿Cómo manejo el reinicio de estado del builder después de build()?
La respuesta barata es no reutilizar builders: una instancia nueva por objeto casi no cuesta nada. Si tienes que reutilizarlo, añade un reset() y documéntalo claramente.
¿Debería el Builder ser una clase separada o una clase estática anidada?
Las clases estáticas anidadas (la convención de Java) mantienen juntos builder y producto; elige una clase separada cuando un builder sirve a varios tipos de producto.
¿Puedo usar Builder con inyección de dependencias?
Claro: pasa los colaboradores por el constructor o los setters del builder como con cualquier otro objeto. Es útil cuando el producto necesita servicios durante la construcción.
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: Ejemplos en Java, Python y TypeScript
Garantiza que una clase tenga una única instancia y ofrece acceso global a ella — el patrón creacional para controlar la creación de objetos, con implementaciones listas para producción.
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: Clona Objetos sin Reconstruirlos
Crea objetos clonando instancias preconfiguradas en lugar de reconstruirlos desde cero. Patrón Prototype con ejemplos en Python, Java y JavaScript.