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 C# exhibe un builder tradicional para construir un menú de restaurante, donde algunos componentes son opcionales.
using System;
using System.Collections.Generic;
public class Menu
{
private Menu(string platoPrincipal, string? bebida, string? postre)
{
PlatoPrincipal = platoPrincipal;
Bebida = bebida;
Postre = postre;
}
public string PlatoPrincipal { get; }
public string? Bebida { get; }
public string? Postre { get; }
public string Resumen()
{
var partes = new List<string> { $"Menu: {PlatoPrincipal}" };
if (!string.IsNullOrWhiteSpace(Bebida)) partes.Add($"bebida: {Bebida}");
if (!string.IsNullOrWhiteSpace(Postre)) partes.Add($"postre: {Postre}");
return string.Join(", ", partes);
}
public sealed class Builder
{
private string? _platoPrincipal;
private string? _bebida;
private string? _postre;
public Builder ConPlatoPrincipal(string platoPrincipal)
{
_platoPrincipal = platoPrincipal;
return this;
}
public Builder ConBebida(string bebida)
{
_bebida = bebida;
return this;
}
public Builder ConPostre(string postre)
{
_postre = postre;
return this;
}
public Menu Construir()
{
if (string.IsNullOrWhiteSpace(_platoPrincipal))
{
throw new InvalidOperationException("El plato principal es obligatorio");
}
return new Menu(_platoPrincipal, _bebida, _postre);
}
}
}
El cliente obtiene un objeto consistente utilizando una API encadenable:
using System;
public static class DemoMenu
{
public static void Main()
{
var menuEjecutivo = new Menu.Builder()
.ConPlatoPrincipal("Pasta al pesto")
.ConBebida("Agua mineral")
.ConPostre("Tiramisu")
.Construir();
Console.WriteLine(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.
using System;
using System.Collections.Generic;
using System.Text;
public class Reporte
{
public Reporte(string contenido)
{
Contenido = contenido;
}
public string Contenido { get; }
}
public interface IReporteBuilder
{
void IniciarDocumento();
void AgregarEncabezado(string titulo);
void AgregarCuerpo(string dato, string valor);
void AgregarPie(string autor);
Reporte ObtenerResultado();
}
public class ReporteJsonBuilder : IReporteBuilder
{
private readonly StringBuilder _json = new StringBuilder();
private bool _primerElemento = true;
public void IniciarDocumento()
{
_json.AppendLine("{");
}
public void AgregarEncabezado(string titulo)
{
_json.AppendLine($" \"titulo\": \"{titulo}\",");
_json.AppendLine(" \"datos\": [");
}
public void AgregarCuerpo(string dato, string valor)
{
if (!_primerElemento)
{
_json.AppendLine(",");
}
_json.Append($" {{ \"{dato}\": \"{valor}\" }}");
_primerElemento = false;
}
public void AgregarPie(string autor)
{
_json.AppendLine();
_json.AppendLine(" ],");
_json.AppendLine($" \"autor\": \"{autor}\"");
_json.AppendLine("}");
}
public Reporte ObtenerResultado()
{
return new Reporte(_json.ToString());
}
}
public class ReporteXmlBuilder : IReporteBuilder
{
private readonly StringBuilder _xml = new StringBuilder();
public void IniciarDocumento()
{
_xml.AppendLine("<reporte>");
}
public void AgregarEncabezado(string titulo)
{
_xml.AppendLine($" <titulo>{titulo}</titulo>");
_xml.AppendLine(" <datos>");
}
public void AgregarCuerpo(string dato, string valor)
{
_xml.AppendLine($" <dato nombre=\"{dato}\">{valor}</dato>");
}
public void AgregarPie(string autor)
{
_xml.AppendLine(" </datos>");
_xml.AppendLine($" <autor>{autor}</autor>");
_xml.AppendLine("</reporte>");
}
public Reporte ObtenerResultado()
{
return new Reporte(_xml.ToString());
}
}
public class DirectorReporte
{
private readonly IReporteBuilder _builder;
public DirectorReporte(IReporteBuilder builder)
{
_builder = builder;
}
public Reporte ConstruirReporte(string titulo, IDictionary<string, string> datos, string autor)
{
_builder.IniciarDocumento();
_builder.AgregarEncabezado(titulo);
foreach (var dato in datos)
{
_builder.AgregarCuerpo(dato.Key, dato.Value);
}
_builder.AgregarPie(autor);
return _builder.ObtenerResultado();
}
}
El cliente selecciona el builder adecuado y utiliza el director para generar el reporte deseado:
using System;
using System.Collections.Generic;
public static class DemoReportes
{
public static void Main()
{
var datos = new Dictionary<string, string>
{
["ventas"] = "1500",
["devoluciones"] = "3"
};
var directorJson = new DirectorReporte(new ReporteJsonBuilder());
var reporteJson = directorJson.ConstruirReporte("Informe mensual", datos, "Equipo Comercial");
Console.WriteLine(reporteJson.Contenido);
var directorXml = new DirectorReporte(new ReporteXmlBuilder());
var reporteXml = directorXml.ConstruirReporte("Informe mensual", datos, "Equipo Comercial");
Console.WriteLine(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.