El patrón Builder separa la construcción de un objeto complejo de su representación final. En lugar de un constructor monolítico con numerosos parámetros, descompone el proceso en pasos controlados, permitiendo crear distintas representaciones del mismo objeto usando el mismo flujo de construcción.
Builder resulta clave cuando los objetos tienen muchas combinaciones opcionales, deben armarse siguiendo un orden estricto o dependen de datos provenientes de fuentes heterogéneas. El patrón incrementa la legibilidad del código y reduce errores al evitar constructores con firmas difíciles de recordar.
Los constructores con largas listas de parámetros son propensos a errores: el orden importa, muchos valores pueden ser opcionales y, si se utilizan setters, existe el riesgo de dejar el objeto en un estado inválido. Además, cuando se necesita crear distintas variantes (por ejemplo, versiones para exportar y para visualizar), duplicar el código de construcción se vuelve inviable.
Builder encapsula el proceso en una entidad dedicada, que conoce cada paso necesario y valida los datos antes de producir el objeto final. El cliente invoca solo los pasos que necesita, lo que mejora la claridad y evita estados inconsistentes.
La intención del patrón es aislar la lógica de armado del producto, ofreciendo una interfaz fluida para configurar sus partes. Se busca:
En frameworks de persistencia y generación de documentos es común encontrar builders encadenables que simplifican la declaración de objetos complejos y favorecen la inmutabilidad.
Los elementos principales del patrón Builder son:
El director es útil cuando se desea reutilizar la secuencia de creación en distintos contextos. No obstante, muchos builders modernos permiten que el cliente llame directamente a los pasos mediante una interfaz fluida.
El siguiente ejemplo en Java exhibe un builder tradicional para construir un menú de restaurante, donde algunos componentes son opcionales.
public class Menu {
private final String platoPrincipal;
private final String bebida;
private final String postre;
private Menu(String platoPrincipal, String bebida, String postre) {
this.platoPrincipal = platoPrincipal;
this.bebida = bebida;
this.postre = postre;
}
public String resumen() {
return "Menú: " + platoPrincipal
+ (bebida != null ? ", bebida: " + bebida : "")
+ (postre != null ? ", postre: " + postre : "");
}
public static class Builder {
private String platoPrincipal;
private String bebida;
private String postre;
public Builder conPlatoPrincipal(String platoPrincipal) {
this.platoPrincipal = platoPrincipal;
return this;
}
public Builder conBebida(String bebida) {
this.bebida = bebida;
return this;
}
public Builder conPostre(String postre) {
this.postre = postre;
return this;
}
public Menu construir() {
if (platoPrincipal == null || platoPrincipal.isBlank()) {
throw new IllegalStateException("El plato principal es obligatorio");
}
return new Menu(platoPrincipal, bebida, postre);
}
}
}
El cliente obtiene un objeto consistente utilizando una API encadenable:
public class Aplicacion {
public static void main(String[] args) {
Menu menuEjecutivo = new Menu.Builder()
.conPlatoPrincipal("Pasta al pesto")
.conBebida("Agua mineral")
.conPostre("Tiramisú")
.construir();
System.out.println(menuEjecutivo.resumen());
}
}
Consideremos un generador de reportes que puede exportar en JSON o XML. Cada formato requiere pasos similares (encabezados, cuerpo, pie), pero la representación final difiere. Implementaremos builders concretos y un director que orquesta la secuencia.
public class Reporte {
private final String contenido;
public Reporte(String contenido) {
this.contenido = contenido;
}
public String contenido() {
return contenido;
}
}
public interface ReporteBuilder {
void iniciarDocumento();
void agregarEncabezado(String titulo);
void agregarCuerpo(String dato, String valor);
void agregarPie(String autor);
Reporte obtenerResultado();
}
public class ReporteJsonBuilder implements ReporteBuilder {
private final StringBuilder json = new StringBuilder();
private boolean primerElemento = true;
@Override
public void iniciarDocumento() {
json.append("{\n");
}
@Override
public void agregarEncabezado(String titulo) {
json.append(" \"titulo\": \"").append(titulo).append("\",\n");
json.append(" \"datos\": [\n");
}
@Override
public void agregarCuerpo(String dato, String valor) {
if (!primerElemento) {
json.append(",\n");
}
json.append(" { \"").append(dato).append("\": \"").append(valor).append("\" }");
primerElemento = false;
}
@Override
public void agregarPie(String autor) {
json.append("\n ],\n");
json.append(" \"autor\": \"").append(autor).append("\"\n");
json.append("}\n");
}
@Override
public Reporte obtenerResultado() {
return new Reporte(json.toString());
}
}
public class ReporteXmlBuilder implements ReporteBuilder {
private final StringBuilder xml = new StringBuilder();
@Override
public void iniciarDocumento() {
xml.append("<reporte>\n");
}
@Override
public void agregarEncabezado(String titulo) {
xml.append(" <titulo>").append(titulo).append("</titulo>\n");
xml.append(" <datos>\n");
}
@Override
public void agregarCuerpo(String dato, String valor) {
xml.append(" <dato nombre=\"").append(dato).append("\">")
.append(valor).append("</dato>\n");
}
@Override
public void agregarPie(String autor) {
xml.append(" </datos>\n");
xml.append(" <autor>").append(autor).append("</autor>\n");
xml.append("</reporte>\n");
}
@Override
public Reporte obtenerResultado() {
return new Reporte(xml.toString());
}
}
public class DirectorReporte {
private final ReporteBuilder builder;
public DirectorReporte(ReporteBuilder builder) {
this.builder = builder;
}
public Reporte construirReporte(String titulo, Map<String, String> datos, String autor) {
builder.iniciarDocumento();
builder.agregarEncabezado(titulo);
datos.forEach(builder::agregarCuerpo);
builder.agregarPie(autor);
return builder.obtenerResultado();
}
}
El cliente selecciona el builder adecuado y utiliza el director para generar el reporte deseado:
public class Ejecucion {
public static void main(String[] args) {
Map<String, String> datos = Map.of("ventas", "1500", "devoluciones", "3");
DirectorReporte directorJson = new DirectorReporte(new ReporteJsonBuilder());
Reporte reporteJson = directorJson.construirReporte("Informe mensual", datos, "Equipo Comercial");
System.out.println(reporteJson.contenido());
DirectorReporte directorXml = new DirectorReporte(new ReporteXmlBuilder());
Reporte reporteXml = directorXml.construirReporte("Informe mensual", datos, "Equipo Comercial");
System.out.println(reporteXml.contenido());
}
}
Gracias al director, la secuencia de pasos se mantiene consistente, mientras que los builders concretos controlan la representación final. Nuevos formatos (YAML, texto plano) solo requieren agregar otro builder.
Existen diversas adaptaciones del patrón:
El patrón agrega capas extra (builders, directores) y puede resultar innecesario si el objeto es simple o tiene pocos parámetros opcionales. Además, cuando se deben introducir nuevos atributos obligatorios, es necesario actualizar todos los builders concretos, lo que requiere disciplina para mantenerlos sincronizados.
Para situaciones donde los objetos son pequeños, un constructor clásico asistido por un objeto de configuración puede resultar suficiente. Builder cobra sentido al mejorar la claridad y la reutilización ante escenarios complejos.
Builder es especialmente valioso cuando:
Si se utiliza en conjunto con otros patrones creacionales, Builder puede apoyarse en Prototype para clonar configuraciones base o en Abstract Factory para seleccionar el builder adecuado según la plataforma.