El Builder es un patrón que separa la construcción paso a paso de un objeto complejo. Resulta útil cuando una clase admite muchas configuraciones o necesita validar el resultado antes de entregarlo.
Todos nos hemos encontrado alguna vez con una clase “monstruo”. Una clase que necesita tanta configuración que instanciarla se convierte en un dolor de cabeza.
Imagina que estamos modelando un PC Gaming. Un ordenador tiene procesador, RAM, disco duro (o varios), tarjeta gráfica (o no), refrigeración líquida, luces RGB, tarjeta WiFi…
El Problema: El Constructor Telescópico
La primera solución que se nos ocurre es hacer un constructor que lo acepte todo.
// ❌ El anti-patrón del Constructor Telescópico
var miPC = new Ordenador(
"Intel i9", // CPU
32, // RAM
"SSD 1TB", // Disco
true, // ¿Tiene RGB?
null, // Sin refrigeración líquida
"Nvidia 4090", // GPU
false // Sin WiFi
);¿Ves el problema?
- Es ilegible: ¿Qué significa ese
trueen el cuarto parámetro? ¿Y elfalsedel final? Tienes que ir a la definición de la clase para saberlo. - Es rígido: Si quieres crear un PC sin tarjeta gráfica, tienes que pasar
null. - Es propenso a errores: Si varios parámetros tienen el mismo tipo, puedes intercambiarlos sin que el compilador lo detecte.
Para eso sirve el Patrón Builder. Su objetivo es separar la construcción de un objeto complejo de su representación, permitiendo crear diferentes representaciones con el mismo proceso de construcción.
La Solución: Construcción paso a paso
En lugar de crear el objeto de golpe, el Builder nos propone crearlo paso a paso. Extraemos la lógica de construcción de la propia clase Ordenador y la movemos a una clase separada llamada OrdenadorBuilder.
Implementación en C# (Estilo Fluido)
En C# y el desarrollo moderno, a menudo simplificamos el patrón usando una Interfaz Fluida (Fluent API). Esto nos permite encadenar métodos, haciendo que el código se lea casi como lenguaje natural.
El Producto
Nuestro objeto complejo. Fíjate que ya no necesitamos un constructor gigante.
public class Ordenador
{
public string CPU { get; set; }
public int RAM { get; set; }
public string Almacenamiento { get; set; }
public string GPU { get; set; }
public bool TieneLucesRGB { get; set; }
public override string ToString()
{
return $"PC: {CPU}, {RAM}GB RAM, {Almacenamiento}, GPU: {GPU ?? "Integrada"}, RGB: {TieneLucesRGB}";
}
}El Builder (Con Fluent Interface)
Creamos una clase encargada de configurar el objeto paso a paso. Cada método devuelve this (el propio builder), permitiendo el encadenamiento.
public class OrdenadorBuilder
{
// Mantenemos una referencia al objeto que estamos construyendo
private Ordenador _ordenador = new Ordenador();
public OrdenadorBuilder ConCPU(string cpu)
{
_ordenador.CPU = cpu;
return this; // Retornamos el builder para encadenar
}
public OrdenadorBuilder ConRAM(int gb)
{
_ordenador.RAM = gb;
return this;
}
public OrdenadorBuilder ConDisco(string disco)
{
_ordenador.Almacenamiento = disco;
return this;
}
public OrdenadorBuilder ConGPU(string gpu)
{
_ordenador.GPU = gpu;
return this;
}
public OrdenadorBuilder ConRGB()
{
_ordenador.TieneLucesRGB = true;
return this;
}
// El paso final: Entregar el producto
public Ordenador Build()
{
if (_ordenador.RAM < 4)
throw new InvalidOperationException("Se necesitan al menos 4 GB de RAM");
Ordenador resultado = _ordenador;
_ordenador = new Ordenador(); // El builder queda listo para reutilizarse
return resultado;
}
}Uso del Patrón (El Cliente)
Mira la diferencia con el constructor del principio. Ahora el código cuenta una historia.
public class Program
{
public static void Main()
{
// ✅ Construcción limpia y legible
var miSuperPC = new OrdenadorBuilder()
.ConCPU("Intel i9")
.ConRAM(32)
.ConDisco("NVMe 2TB")
.ConGPU("RTX 4090")
.ConRGB() // Método opcional
.Build();
Console.WriteLine(miSuperPC);
}
}Esta técnica es la que usáis constantemente en .NET, por ejemplo, al configurar el WebHostBuilder en ASP.NET Core o al añadir servicios con services.AddMvc().AddJsonOptions(...).
El Director (Opcional)
A veces queremos encapsular “recetas” conocidas. Si en nuestra tienda vendemos un “PC Básico” y un “PC Gamer”, no queremos repetir los pasos del Builder cada vez.
Podemos crear una clase Director (o simplemente métodos factoría estáticos) que use el Builder por nosotros.
public class DirectorOrdenadores
{
public Ordenador ConstruirPCBasico()
{
return new OrdenadorBuilder()
.ConCPU("Intel i3")
.ConRAM(8)
.ConDisco("HDD 500GB")
.Build();
}
public Ordenador ConstruirPCGamer()
{
return new OrdenadorBuilder()
.ConCPU("Ryzen 9")
.ConRAM(32)
.ConDisco("SSD 1TB")
.ConGPU("RTX 3080")
.ConRGB()
.Build();
}
}Ventajas y Desventajas
| Ventajas | Desventajas |
|---|---|
| Legibilidad: El código cliente es auto-explicativo. | Verbosidad: Requiere crear clases extra (el Builder). |
Inmutabilidad: Puedes hacer que el Build() devuelva un objeto inmutable, asegurando que nadie lo modifique después de creado. | Complejidad: Para clases con 3 parámetros, es sobreingeniería. |
| Flexibilidad: Permite construir representaciones diferentes usando el mismo proceso. |
Builder vs Factory
Es fácil confundirlos porque ambos crean objetos, pero la diferencia es clave:
- Factory Method / Abstract Factory: Crean el objeto de una sola vez (“toma, tu coche”). Son buenos cuando la creación no requiere muchos pasos o configuración externa.
- Builder: Crea el objeto paso a paso (“Pon el motor, ahora las ruedas, ahora píntalo…”). Es ideal cuando el objeto tiene muchas partes opcionales o el proceso de creación es complejo.