patron-builder

Patrón Builder: objetos complejos paso a paso

  • 5 min

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
);
Copied!

¿Ves el problema?

  1. Es ilegible: ¿Qué significa ese true en el cuarto parámetro? ¿Y el false del final? Tienes que ir a la definición de la clase para saberlo.
  2. Es rígido: Si quieres crear un PC sin tarjeta gráfica, tienes que pasar null.
  3. 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}";
    }
}
Copied!

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;
    }
}
Copied!

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);
    }
}
Copied!

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();
    }
}
Copied!

Ventajas y Desventajas

VentajasDesventajas
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.