Introducción al patrón Builder
El patrón Builder es un patrón creacional pensado para construir objetos complejos paso a paso, evitando constructores telescópicos y mejorando la legibilidad y mantenibilidad. En lugar de pasar 10 parámetros opcionales a un constructor, se utiliza un objeto "builder" que va configurando la instancia y al final ejecuta un método build() que devuelve el objeto final.
¿Cuándo usar Builder?
- Cuando un objeto tiene muchos parámetros opcionales o combinaciones posibles.
- Cuando quieres separar la construcción del objeto de su representación.
- Cuando necesitas crear variaciones de un objeto de forma legible y testeable.
Pasos para implementar Builder
- Identificar la clase del objeto complejo (p. ej.
Order,HttpRequest,UserProfile). - Definir las propiedades inmutables o configurables.
- Crear una clase
Builderque exponga métodos encadenables (fluent) para cada propiedad opcional. - Validar en
build()las combinaciones requeridas y devolver la instancia construida. - Preferir objetos inmutables para evitar efectos secundarios después de construido el objeto.
Ejemplo práctico en TypeScript
A continuación un ejemplo de un Email construido con Builder. Muestra cómo manejar campos obligatorios, opcionales y validaciones simples.
class Email {
readonly to: string;
readonly subject: string;
readonly body: string;
readonly cc?: string[];
readonly attachments?: string[];
private constructor(builder: EmailBuilder) {
this.to = builder.to;
this.subject = builder.subject;
this.body = builder.body;
this.cc = builder.cc;
this.attachments = builder.attachments;
}
static get Builder() {
return new EmailBuilder();
}
}
class EmailBuilder {
to!: string; // obligatorio
subject: string = '';
body: string = '';
cc?: string[];
attachments?: string[];
withTo(to: string) {
this.to = to;
return this;
}
withSubject(subject: string) {
this.subject = subject;
return this;
}
withBody(body: string) {
this.body = body;
return this;
}
withCc(cc: string[]) {
this.cc = cc;
return this;
}
withAttachments(attachments: string[]) {
this.attachments = attachments;
return this;
}
build(): Email {
if (!this.to) throw new Error('Email <span class="token string">"to" es obligatorio'</span>);
return new Email(this);
}
}
// Uso
const email = Email.Builder
.withTo('[email protected]')
.withSubject('Deploy')
.withBody('Se desplegó en producción')
.build();Ejemplo práctico en Java
En Java solemos usar la clase estática interna Builder para que el uso sea más expresivo y encapsulado.
public class Pizza {
private final String size; // obligatorio
private final boolean cheese;
private final boolean pepperoni;
private final boolean bacon;
private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.bacon = builder.bacon;
}
public static class Builder {
private final String size;
private boolean cheese = false;
private boolean pepperoni = false;
private boolean bacon = false;
public Builder(String size) {
this.size = size;
}
public Builder cheese(boolean value) {
this.cheese = value;
return this;
}
public Builder pepperoni(boolean value) {
this.pepperoni = value;
return this;
}
public Builder bacon(boolean value) {
this.bacon = value;
return this;
}
public Pizza build() {
// ejemplo de validación
if (!"small".equals(size) && !"medium".equals(size) && !"large".equals(size)) {
throw new IllegalStateException("size debe ser small|medium|large");
}
return new Pizza(this);
}
}
}
// Uso
Pizza pizza = new Pizza.Builder("medium")
.cheese(true)
.pepperoni(true)
.build();Ventajas y consideraciones
- Mejora la legibilidad respecto a constructores con muchos parámetros.
- Facilita la creación de objetos inmutables.
- Permite validaciones centralizadas en
build(). - Puede producir clases adicionales (algo de boilerplate). En Java se suele reducir con Lombok (
@Builder) — si tu proyecto lo permite.
¿Qué diferencia al Builder de un Factory?
Factory abstrae la creación de objetos (puede devolver subclases) y encapsula la lógica de elección del tipo; Builder se centra en construir una única estructura compleja paso a paso. Ambos pueden combinarse.
¿Cómo pruebo objetos creados con Builder?
Usa builders en tus tests para crear fixtures rápidamente (arrange). Puedes implementar builders específicos para pruebas que expongan defaults útiles y solo modifiquen lo necesario.
¿El Builder afecta el rendimiento?
La sobrecarga es mínima: creación de objetos extra (el builder) y llamadas encadenadas. En la mayoría de aplicaciones esa diferencia es despreciable frente a claridad y mantenibilidad.
¿Puedo reutilizar un builder para construir varios objetos?
Sí, pero ten cuidado con el estado mutado; preferible crear uno nuevo o clonar el estado inicial.
¿Cómo podría manejar parámetros obligatorios?
Pasarlos al constructor del Builder (ver ejemplo Java) o validar en build().