Patrón Builder en c#.net


patrón builder c# .net


El patrón de diseño Builder es uno de los veintitrés patrones descritos en el libro "Design Patterns: Elements of Reusable Object-Oriented Software" de los autores conocidos como Gang of Four (GoF). 

En este post voy a explicar todo lo que hay que saber sobre el patrón desde un punto de vista c#.net.

La traducción de Builder al español sería Constructor, pero para no confundir con el constructor de una clase y para relacionarlo con el nombre por el cual se conoce, prefiero no traducirlo.

¿QUé es el patrón de diseño Builder?

La definición oficial del libro es la siguiente:

El patrón de diseño Builder separa la creación de un objeto complejo de su representación de modo que el mismo proceso de construcción pueda crear representaciones diferentes.

Básicamente significa que en lugar de implementar la creación de un objeto en el constructor de la clase, va a haber otras clases encargadas de crear el objeto y asignarles las propiedades iniciales. Cada una de estas "otras" clases será un builder. Y cada uno de estos builders será capaz de crear un objeto nuevo con ciertas características.

Dentro de la clasificación de los patrones, Builder es considerado un patrón creacional.

Motivación · ¿Cuándo usar el PAtrón BUILDEr?

Supongamos que necesitas construir un mismo objeto complejo muchas veces y además con diferentes configuraciones, por ejemplo, una pizza. Cada pizza de la carta va a tener características concretas y diferenciadas de las demás:

       public class Pizza
        {
            private readonly string _masa;
            private readonly string _salsa;
            private readonly string _relleno;
            private readonly string _tamaño;

            public Pizza(string tamaño, string masa, string salsa, string relleno)
            {
                _tamaño = tamaño;
                _masa = masa;
                _salsa = salsa;
                _relleno = relleno;
            }
        }

Si no queremos tener que recordar cada vez que creemos una pizza "CuatroQuesos" su tamaño, masa, salsa y relleno, podríamos utilizar el patrón Builder y olvidarnos de sus parámetros.

Para cada pizza del menú necesitaríamos un builder.

A modo orientativo, si tienes dudas sobre cuándo usar el patrón Builder fíjate en el número de parámetros que necesitas para crear el producto final. Si es cuatro o superior te podría interesar, por debajo no merece la pena.

Nota: he puesto un ejemplo sencillo, hay que suponer que las propiedades de la Pizza en lugar de cuatro strings, podrían ser cuatro clases complejas como Masa(), Salsa(), Relleno() y Tamaño().

Estructura · Diagrama de Clases

Participantes

La estructura y los participantes empezarán a tener sentido cuando los veamos aplicados más adelante en el código de ejemplo:

  • Builder 
    • clase abstracta para crear los objetos finales producto.
    • ejemplo en c#: crear una clase abstracta BuilderPizza
  • BuilderConcreto 
    • selecciona un nombre que defina la representación del objeto a crear. 
    • implementa la clase abstracta  Builder
    • cconstruye y ensambla las partes del producto.
    • ejemplo en c#: clase BuilderPizzaHawaiana : BuilderPizza
  • Director
    • construye un objeto usando la clase Builder.
    • ejemplo en c#: clase PreparadorPizza o Cocinero o Cocina
  • Producto
    • representa el objeto complejo en construcción. El ConstructorConcreto construye la repesentación interna del producto y define el proceso de ensamblaje.
    • incluye las clases que definen sus partes constituyentes, incluyendo interfaces para ensamblar partes en el resultado final.
    • ejemplo en c#: objeto creado PizzaCuatroQuesosPizzaHawaiana

Colaboraciones

  1. El cliente crea el objeto Director (Cocina o PreparadorPizzas) y lo configura con el objeto builder deseado
  2. El Director notifica al builder cada vez que hay que construir una parte de un producto.
  3. El Builder maneja las peticiones del director y las añade al producto.
  4. El cliente obtiene el producto del builder.

Diagrama de interacción:

// 1. El cliente crea el objeto director
var cocina = new Cocina();

// y lo configura con el objeto builder deseado
cocina.RecepcionarProximaPizza(new CuatroQuesosBuilder("Familiar"));
           
//2. El director notifica al builder que hay que crear el objeto paso a paso
 cocina.CocinarPizzaPasoAPaso();

//3. El clinte obtiene el producto del builder (en este caso a través del director)
var pizzaCuatroQuesos = cocina.PizzaPreparada;


VENTAJAS Y DESVENTAJAS

Ventajas

  • Permite variar la representación interna de un producto. Esconde los detalles de la construcción del producto y cómo se ensambla. Lo que hay que hacer para cambiar la representación interna del producto es definir un nuevo tipo de builder.
     
  • Encapsula el código de construcción y de representación. Aumenta la modularidad. Los clientes no necesitan saber nada sobre las clases que definen la estructura interna del producto.
     
  • Proporciona un control más explícito sobre el proceso de construcción. A diferencia de los demás patrones de creación, que construyen los productos una sola vez, el patrón Builder construye el producto paso a paso, bajo el control del Director.
     
  • El código es más mantenible si el número de parametros para crear el objeto es mayor que cuatro.

Desventajas

  • Hay que crear un BuilderConrecto para cada representación de un producto, lo que puede acabar con multitud de clases.
     
  • Las clases productos que construye el Builder deben ser mutables.

    Por ejemplo, la clase pizza que habímos definido era inmutable puesto que una vez se ha creado ya no se puede modificar. Pues si  vas a utilizar un builder esto ya no va a ser posible.
     
         public class Pizza
            {
                private readonly string _masa;
                private readonly string _salsa;
                private readonly string _relleno;
                private readonly string _tamaño;
    
                public Pizza(string tamaño, string masa, string salsa, string relleno)
                {
                    _tamaño = tamaño;
                    _masa = masa;
                    _salsa = salsa;
                    _relleno = relleno;
                }
            }
    
            public class Pizza
            {
                public string Masa { get; set; }
                public string Salsa { get; set; }
                public string Relleno { get; set; }
                public string Tamaño { get; set; }
    
                public Pizza()
                {
                    
                }
    
                public Pizza(string tamaño, string masa, string salsa, string relleno) : this()
                {
                    Tamaño = tamaño;
                    Masa = masa;
                    Salsa = salsa;
                    Relleno = relleno;
                }
            }
    
    
     
  • No garantiza que los campos de la clase producto estén inicializados

    Efectivamente, en la nueva versión de pizza necesitamos un constructor vacío, con lo que ya no se garantiza que al tener un objeto Pizza, todos sus campos estén inicializados.

    Un cliente podría hacer lo siguiente:
     
    var pizza  = new Pizza();
    MostrarSalsaPorPantalla(pizza.Salsa) // podría producirse una excepción 
    //puesto que la propiedad Salsa no está inicializada
    
    
     
  • No es muy compatible con la inyección de dependencias. A mi entender, y que alguien me corrija, esto no es una desventaja. No creo que sea una buena práctica inyectar este patrón en clases servicio o de aplicación. Es mejor utilizarlo tal cual para crear objetos de dominio.
     
  • Requiere emplear más código de lo que sería deseable. En el ejemplo que he puesto no se da, pero si el objeto final tiene muchas propiedades es posible que acabes duplicando parte del código. No todos los builder necesitarán aplicar cada uno de los pasos, y si eso pasa duplicaremos métodos vacíos.

    Por ejemplo, la PizzaCalzone requerirá un paso más que podríamos llamar PasoDoblarPizza(). Este nuevo paso lo deberán implementar todos los builders, pero solo tendrá sentido en el CalzoneBuilder.
     
  • Aplicado al pie de la letra el patrón está "acoplado temporalmente". Es decir, los métodos se deben ejecutar en un orden concreto y si no es así, el resultado no será el deseado:
     
     var cocina = new Cocina();
     
     cocina.RecepcionarProximaPizza(new CuatroQuesosBuilder("Familiar"));
     cocina.CocinarPizzaPasoAPaso();
     var pizzaCuatroQuesos = cocina.PizzaPreparada;
    
    
    Si primero ejecutase CocinarPizzaPasoAPaso() en lugar de RecepcionarProximaPizza() el código sería incorrecto.

    Pero esto es fácilmente solucionable se encapsulamos estas tres sentencias dentro de una única función de Cocina
     public Pizza CocinarPizza(PizzaBuilder pizzaBuilder)
     {
          RecepcionarProximaPizza(pizzaBuilder);
          CocinarPizzaPasoAPaso();
          return PizzaPreparada;
     }
    

    Y llamamos directamente a este método así:
     var cocina = new Cocina();
     
     var pizzaCuatroQuesos = cocina.CocinarPizza(new CuatroQuesosBuilder("Familiar"));
    
    

Sigue siendo el mismo patrón, únicamente encapsulamos la parte de código que tiene acoplamiento temporal.

Implementación

Normalmente hay una clase abstracta Builder que define una operación por cada componente que puede ser creado. 

Ejemplo:

 public abstract class PizzaBuilder
        {
            // Protected para que las clases que implementen puedan acceder
            protected Pizza _pizza;

            // Los builder también puede tener propiedades e inicializarlas
            // en el constructor
            public string Tamaño { get;private set; }

            public Pizza ObtenerPizza() { return _pizza; }

            // Un paso para cada una de las propiedades
            public abstract void PasoPrepararMasa();
            public abstract void PasoAñadirSalsa();
            public abstract void PasoPrepararRelleno();
            public abstract void PasoDoblarPizza();
        }

La clase BuilderConcreto sobreescribe las operaciones para los componentes que está interesado en crear.

Ten en cuenta estas conisderaciones al crear este patrón:

  • Interface de ensamblaje y construcción: Los builders contruyen los productos paso a paso, por tanto, el interface de builder debe ser lo suficientemente general como para permitir construir los productos por partes de todos los tipos de BuildersConcretos.
     
  • ¿Por qué no usar clases abstractas para los productos? En general, los productos creados por los Builders tienen representaciones tan diferentes que sería de poca ayuda definir una clase padre para los diferentes productos.
     
  • Métodos vacíos de manera predeterminada en el constructorSi los métodos de la creación de las partes se dejan vacíos, entonces las clases BuildersConcretos solo necesitan sobreescribir los métodos que les interesa. 

    ¡Vaya! En ese caso debo modificar el PizzaBuilder anterior:
         public abstract class PizzaBuilder
            {
               
                protected Pizza _pizza;
                public string Tamaño { get; set; }
    
                public Pizza ObtenerPizza() { return _pizza; }
    
                public virtual void PasoPrepararMasa()
                {
    
                }
    
                public virtual void PasoAñadirSalsa()
                {
    
                }
    
                public virtual void PasoPrepararRelleno()
                {
    
                }
    
                public virtual void PasoDoblarPoizza()
                {
    
                }
    
            }
    
    
     

Código de ejemplo

El código definitivo en c#.net del patrón Builder quedaría así:

 class Program
    {
        static void Main(string[] args)
        {
            var cocina = new Cocina();

            // un cliente pide una Pizza cuatro quesos familiar
            cocina.RecepcionarProximaPizza(new CuatroQuesosBuilder("Familiar"));
            cocina.CocinarPizzaPasoAPaso();
            var pizzaCuatroQuesos = cocina.PizzaPreparada;

       
            // otro cliente pide una Hawaiana
            cocina.RecepcionarProximaPizza(new HawaianaBuilder("Mediana"));
            cocina.CocinarPizzaPasoAPaso();
            var pizzaHawaiana = cocina.PizzaPreparada;

            // o en lugar de utilizar funciones acopladas temporalmente
            // utilizar una única función
            var pizzaHawaianaRapida = cocina.CocinarPizza(new HawaianaBuilder("Mediana"));
            
        }

        // Producto final
        public class Pizza
        {
            public string Masa { get; set; }
            public string Salsa { get; set; }
            public string Relleno { get; set; }
            public string Tamaño { get; set; }
            public bool EstaDoblada { get; set; }

            public Pizza()
            {
                
            }

            public Pizza(string tamaño, string masa, string salsa, string relleno, bool doblar) : this()
            {
                Tamaño = tamaño;
                Masa = masa;
                Salsa = salsa;
                Relleno = relleno;
                EstaDoblada = doblar;
            }
        }

        // Builder
        public abstract class PizzaBuilder
        {
            // Protected para que las clases que implementen puedan acceder
            protected Pizza _pizza;
            public string Tamaño { get; set; }

            public Pizza ObtenerPizza() { return _pizza; }



            // Un paso para cada una de las propiedades
            public virtual void PasoPrepararMasa()
            {

            }

            public virtual void PasoAñadirSalsa()
            {

            }

            public virtual void PasoPrepararRelleno()
            {

            }

            public virtual void PasoDoblarPoizza()
            {

            }

        }

        // BuilderConcreto
        public class HawaianaBuilder : PizzaBuilder
        {
            public HawaianaBuilder(string tamaño)
            {
                _pizza = new Pizza
                {
                    Tamaño = tamaño
                };
            }
            public override void PasoPrepararMasa()
            {
                _pizza.Masa = "Suave";
            }

            public override void PasoAñadirSalsa()
            {
                _pizza.Salsa = "Dulce";
            }

            public override void PasoPrepararRelleno()
            {
                _pizza.Relleno = "piña+tomate+jamón";
            }
        }

        // Otro BuilderConcreto
        public class CuatroQuesosBuilder : PizzaBuilder
        {
            public CuatroQuesosBuilder(string tamaño)
            {
                _pizza = new Pizza
                {
                    Tamaño = tamaño
                };
            }
            public override void PasoPrepararMasa()
            {
                _pizza.Masa = "Cocido";
            }

            public override void PasoAñadirSalsa()
            {
                _pizza.Salsa = "Roquefort";
            }

            public override void PasoPrepararRelleno()
            {
                _pizza.Relleno = "mozzarela+gorgonzola+parmesano+ricotta";
            }
        }

        // Director
        public class  Cocina
        {
            private PizzaBuilder _pizzaBuilder;
            
            public void RecepcionarProximaPizza(PizzaBuilder pizzaBuilder)
            {
                _pizzaBuilder = pizzaBuilder;
            }

            public void CocinarPizzaPasoAPaso()
            {
                _pizzaBuilder.PasoPrepararMasa();
                _pizzaBuilder.PasoAñadirSalsa();
                _pizzaBuilder.PasoPrepararRelleno();
            }

            public Pizza PizzaPreparada
            {
                get { return _pizzaBuilder.ObtenerPizza(); }
            
            }
        }
    }


Usos conocidos en .net

En .net framework hay pocos usos conocidos del patrón Builder.

  • SqlConnectionStringBuilder: se usa para poder añadir partes de la cadena de conexion por separado. 
  • UriBuilder: parecido al anterior, se usa para crear url's indicando partes de la misma, puerto, host, etc.

Y con .net core hay un ejemplo de builder que siempre nos vamos a encontrar en una aplicación web

  • IWebHostBuilder: la manera de utilizar el builder no es definida del patrón original, pero el concepto es el mismo. A través del builder se puede obtener un IWebHost configurado adecuadamente y que puede ser ejecutado.
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup>startup<();
    }


Fluent Builder

El patrón Builder en c# se puede aplicar de manera fluida. En este caso, se conoce como Fluent Builder.

En lugar de tener un builder específico para cada producto solo es necesario tener un único builder. La clave es que cada uno de los métodos que crean las partes del producto final devuelvan el builder como resultado. De este modo, los pasos se pueden encadenar y nos permite crear productos de un modo más personalizado.

Vamos a mostrar cómo sería el WebHost en el mundo de las pizzas:

 class Program
    {
        static void Main(string[] args)
        {
             var pizzaPersonalizada = PizzaFluentBuilder.Crear()
                                        .ConMasaSuave()
                                        .ConSalsaRoquefort()
                                        .AñadirMozzarela()
                                        .AñadirParmesano()
                                        .Cocinar();
         }
}

Para poder utilizar el Fluent Builder en c# de esta manera habría que definirlo así:

   public class PizzaFluentBuilder
        {
            private readonly Pizza _pizza;

            public static PizzaFluentBuilder Crear()
            {
                return new PizzaFluentBuilder();
            }

            private PizzaFluentBuilder()
            {
                _pizza = new Pizza();
            }
           
            public PizzaFluentBuilder ConMasaSuave()
            {
                _pizza.Masa = "Suave";
                return this;                
            }

            public PizzaFluentBuilder ConMasaCocida()
            {
                _pizza.Masa = "Cocido";
                return this;
             }

            public PizzaFluentBuilder ConSalsaRoquefort()
            {
                _pizza.Salsa = "Roquefort";
                return this;                
            }

            public PizzaFluentBuilder ConSalsaPicante()
            {
                    _pizza.Salsa = "Picante";
                    return this;
             }

            public PizzaFluentBuilder AñadirMozzarela()
            {
                _pizza.Relleno = _pizza.Relleno + "+mozzarela";
                return this;
            }

            public PizzaFluentBuilder AñadirParmesano()
            {
                _pizza.Relleno = _pizza.Relleno + "+parmesano";
                return this;
            }

            public Pizza Cocinar()
            {
                return _pizza;
            }
        }

En general es más habitual encontrar Fluent Builders que los Builders originales. Mi recomendación es empezar utilizando esta variación del patrón. 

Ventajas:
  • La principal ventaja es que el código se entiende mucho mejor. 
  • Solo es necesario tener un único builder.
  • Se puede crear todo tipo de productos finales aplicando diferentes pasos encadenadamente. No es necesario tener productos predeterminados. 
Desventajas
  • El builder tiene que tener todas los pasos y cada una de sus representaciones para poder crear cualquier combinación y en consecuencia podríamos acabar creando un builder gigantesco.

Comparación: Builder vs Fluent Builder vs Constructor

Veamos una comparativa entre las tres maneras que tendríamos de crear un objeto Pizza


  class Program
    {
        static void Main(string[] args)
        {
           // usando el costructor
            var pizzaCuatroQuesosArtesanal = new Pizza(
                                   tamaño: "Familiar",
                                   masa: "Cocido",
                                   salsa: "Roquefort",
                                   relleno: "mozzarela+gorgonzola+parmesano+ricotta");

            
            // con el patrón Builder
            var cocina = new Cocina();
            cocina.RecepcionarProximaPizza(new CuatroQuesosBuilder("Familiar"));
            cocina.CocinarPizzaPasoAPaso();
            var pizzaCuatroQuesos = cocina.PizzaPreparada;


            // con el patrón Fluent Builder 
            var pizzaPersonalizada = PizzaFluentBuilder.Crear()
                                        .ConMasaSuave()
                                        .ConSalsaRoquefort()
                                        .AñadirMozzarela()
                                        .AñadirParmesano()
                                        .Cocinar();
  }
}

Patrones Relacionados

El patrón Abstract Factory (Factoria abstracta) se parece al Builder porque también construye objetos complejos.

¿Cuáles son la principales diferencias entre Builder y Factory?

  • Builder se centra en construir un objeto complejo paso a paso mientras que el Abstract Factory hace hincapié en familias de objetos. Builder devuelve el producto como paso final, pero en lo que respecta a Abstract Factory, el producto se devuelve inmediatamente.
     
  • Es habitual que lo que acaba construyendo el patrón Builder sea un objeto Composite (Compuesto).
     
  • Lo habitual, es que los diseños empiecen aplicando el patrón Método Factoria ya que es menos complicado y más fácil de configurar. En caso de proliferar en número de subclases, la evolución del código es hacia Abstract Factory, Prototype o Builder según la flexibilidad que se necesite.

Fuentes

Libros

Código Utilizado

Aquí tienes un enlace al GitHub donde he dejado el código que he utilizado para este post.

Patrones diseño C# (GitHub)




Quizá algun día empiece a enviar una newsletter, si te gustaría recibirla subscríbete aquí

Archivo