Patrones de diseño en la vida real: Aplicando Builder Pattern con .NET
Aprende y aplica el patrón de diseño Constructor o Builder en tus proyectos de .NET con C# y aplica las mejores prácticas de la industria 🔥

Este patrón de diseño es sencillo pero poderoso, su poder radica en la practicidad y la flexibilidad que nos otorga cuando creamos objetos de Clases que son complejas.

Existen situaciones en las que tenemos que crear objetos muy complejos, inclusive que tienen propiedades que a su vez son objetos de otra clase, es decir tiene objetos compuestos dentro así que necesitaríamos crear primero los objetos compuestos para poder asignarlo al objeto de la clase principal, todo esto puede crear un caos y ser muy tedioso.

Allí es donde entra este patrón, ya que añade una capa de abstracción y hace que el desarrollador no se preocupe más de objetos compuestos ni nada de eso, sino que le pasa los datos necesarios y el "builder" se encarga de crear todo lo necesario por detrás.

Caso práctico

Estamos desarrollando un sistema de nóminas, entonces una de nuestras entidades principales va a ser Empleado

Y como sabrás cuando registramos un nuevo empleado, vamos a guardar algunos datos un poco complejos como: su área, cargo, su dirección domiciliaria y no uno sino una lista de teléfonos.

Esta información van a dar lugar entonces a otras entidades, conocidas como entidades débiles.

Todo esto da como resultado las siguientes entidades:

  • Empleado
  • Area
  • Cargo
  • Direccion
  • Telefono

Código sin aplicar Builder Pattern

Empecemos con las entidades simples:

Área:

    public class Area
    {
        public int IdArea { get; set; }
        public string NombreArea { get; set; }
        public List<Cargo> Cargos { get; set; }

        public Area(int IdArea)
        {
            this.IdArea = IdArea;
            this.NombreArea = NombreArea;
        }
        public Area(string NombreArea)
        {
            this.NombreArea = NombreArea;
        }
        public Area(int IdArea, string NombreArea, List<Cargo> Cargos)
        {
            this.IdArea = IdArea;
            this.NombreArea = NombreArea;
            this.Cargos = Cargos;
        }

        public override string ToString()
        {
            return $"Id:{IdArea}, Nombre:{NombreArea}";
        }
    }

Cargo:

    public class Cargo
    {
        public int IdCargo { get; set; }
        public string NombreCargo { get; set; }
        public Area Area { get; set; }

        public Cargo(int IdCargo)
        {
            this.IdCargo = IdCargo;
        }
        public Cargo(string NombreCargo)
        {
            this.NombreCargo = NombreCargo;
        }
        public Cargo(int IdCargo, string NombreCargo, Area Area)
        {
            this.IdCargo = IdCargo;
            this.NombreCargo = NombreCargo;
            this.Area = Area;
        }
        public override string ToString()
        {
            return $"Id:{IdCargo}, Nombre:{NombreCargo}";
        }
    }

Dirección:

       public class Direccion
    {
        public int IdDireccion { get; set; }
        public string Domicilio { get; set; }
        public string Ciudad { get; set; }
        public string Pais { get; set; }
        public string CodigoPostal { get; set; }
        public Direccion(int IdDireccion)
        {
            this.IdDireccion = IdDireccion;
        }
        public Direccion(string Domicilio, string Ciudad, string Pais, string CodigoPostal)
        {
            this.Domicilio = Domicilio;
            this.Ciudad = Ciudad;
            this.Pais = Pais;
            this.CodigoPostal = CodigoPostal;
        }

        public override string ToString()
        {
            return $"Id:{IdDireccion}, Domicilio:{Domicilio}, Ciudad:{Ciudad}, Pais:{Pais}, CP:{CodigoPostal}";
        }
    }

Teléfono:

    public class Telefono
    {
        public int IdTelefono { get; set; }
        public int CodigoInternacional { get; set; }
        public int NumeroTelefonico { get; set; }
        public string TipoTelefono { get; set; }
        public Telefono(int IdTelefono)
        {
            this.IdTelefono = IdTelefono;
        }
        public Telefono(int CodigoInternacional, int NumeroTelefonico, string TipoTelefono)
        {
            this.CodigoInternacional = CodigoInternacional;
            this.NumeroTelefonico = NumeroTelefonico;
            this.TipoTelefono = TipoTelefono;
        }

        public override string ToString()
        {
            return $"Id:{IdTelefono}, Codigo:{CodigoInternacional}, Numero:{NumeroTelefonico}, Tipo:{TipoTelefono}";
        }
    }

Ahora la entidad con propiedades complejas:

Empleado

    public class Empleado
    {
        public int IdEmpleado { get; set; }
        public string Nombre { get; set; }
        public DateTime FechaIngreso { get; set; }
        public Area Area { get; set; }
        public Cargo Cargo { get; set; }
        public Direccion Direccion { get; set; }
        public List<Telefono> Telefonos { get; set; }

        #region Constructores
        public Empleado(int IdEmpleado, string Nombre, DateTime FechaIngreso, Area Area, Cargo Cargo, Direccion Direccion, List<Telefono> Telefonos)
        {
            this.IdEmpleado = IdEmpleado;
            this.Nombre = Nombre;
            this.FechaIngreso = FechaIngreso;
            this.Area = Area;
            this.Cargo = Cargo;
            this.Direccion = Direccion;
            this.Telefonos = Telefonos;
        }

        public Empleado(int IdEmpleado, string Nombre, DateTime FechaIngreso)
        {
            this.IdEmpleado = IdEmpleado;
            this.Nombre = Nombre;
            this.FechaIngreso = FechaIngreso;
        }
        public Empleado()
        {

        }
        #endregion

        public override string ToString()
        {
            return $@"Empleado =>
            Nombre:{Nombre}
            FechaIngreso:{FechaIngreso}
            Area => {Area}
            Cargo => {Cargo}
            Direccion => {Direccion}
            Telefonos => {string.Join(" || ", Telefonos)}";
        }    
    }

Nota que he sobre-escrito el método ToString(), esto es para poder visualizar mejor la data en la consola cuando hagamos las pruebas

Probamos con la consola:

Console.WriteLine("Example without Builder pattern!");

//Creo el objeto empleado
Empleado empleado = new Empleado();
empleado.IdEmpleado = 123;
empleado.Nombre = "Gerson";
empleado.FechaIngreso = new DateTime(2020, 12, 20);

//Creo los objetos compuestos de empleado
Area area = new Area("IT");
Cargo cargo = new Cargo(".NET Engineer");
Direccion direccion = new Direccion("Calle Begonias 23","Lima","Peru","54321");
List<Telefono> telefonos = new List<Telefono>(){
    new Telefono(51, 789545521, "Móvil"),
    new Telefono(1, 985552000, "Whatsapp"),
    new Telefono(51, 8526320, "Fijo")
};
//Asigno los objetos compuestos al objeto principal empleado
empleado.Area = area;
empleado.Cargo = cargo;
empleado.Direccion = direccion;
empleado.Telefonos = telefonos;

Console.WriteLine(empleado);

En el terminal tenemos esto:

Si bien el objeto empleado se ha poblado correctamente, nota cómo hemos tenido que crear otros objetos para poder poblarlo bien, además de no olvidarnos de asignarlos y lidiar con listas. Esto puede ser muy tedioso y propenso a errores.

Habrá una mejor forma de construir objetos? Foto de Shane McLendon en Unsplash

Solución aplicando Builder Pattern

Las entidades débiles no van a sufrir cambios, lo que sí va a cambiar es:

  • La entidad Empleado ahora va a tener una clase anidada llamada EmpleadoBuilder
  • Tendremos la interfaz IBuilder con Generics que servirá como contrato para EmpleadoBuilder

Clase anidada en C# es una clase dentro de otra. En Java se llamaría inner class.

Empleado

   public class Empleado
    {
        public int IdEmpleado { get; set; }
        public string Nombre { get; set; }
        public DateTime FechaIngreso { get; set; }
        public Area Area { get; set; }
        public Cargo Cargo { get; set; }
        public Direccion Direccion { get; set; }
        public List<Telefono> Telefonos { get; set; }

        #region Constructores
        public Empleado(int IdEmpleado, string Nombre, DateTime FechaIngreso, Area Area, Cargo Cargo, Direccion Direccion, List<Telefono> Telefonos)
        {
            this.IdEmpleado = IdEmpleado;
            this.Nombre = Nombre;
            this.FechaIngreso = FechaIngreso;
            this.Area = Area;
            this.Cargo = Cargo;
            this.Direccion = Direccion;
            this.Telefonos = Telefonos;
        }

        public Empleado(int IdEmpleado, string Nombre, DateTime FechaIngreso)
        {
            this.IdEmpleado = IdEmpleado;
            this.Nombre = Nombre;
            this.FechaIngreso = FechaIngreso;
        }
        public Empleado()
        {

        }
        #endregion

        public override string ToString()
        {
            return $@"Empleado =>
            Nombre:{Nombre}
            FechaIngreso:{FechaIngreso}
            Area => {Area}
            Cargo => {Cargo}
            Direccion => {Direccion}
            Telefonos => {string.Join(" || ", Telefonos)}";
        }

        public class EmpleadoBuilder : IBuilder<Empleado>
        {
            private int IdEmpleado;
            private string Nombre;
            private DateTime FechaIngreso;
            private Area Area;
            private Cargo Cargo;
            private Direccion Direccion;
            private List<Telefono> Telefonos = new List<Telefono>();

            public EmpleadoBuilder()
            {

            }

            public EmpleadoBuilder SetId(int IdEmpleado)
            {
                this.IdEmpleado = IdEmpleado;
                return this;
            }

            public EmpleadoBuilder SetNombre(string Nombre)
            {
                this.Nombre = Nombre;
                return this;
            }

            public EmpleadoBuilder SetFechaIngreso(DateTime FechaIngreso)
            {
                this.FechaIngreso = FechaIngreso;
                return this;
            }
            //Setear area cuando ya existe
            public EmpleadoBuilder SetArea(int IdArea)
            {
                Area = new Area(IdArea);
                return this;
            }
            //Setear y crear una área
            public EmpleadoBuilder SetArea(string NombreArea)
            {
                Area = new Area(NombreArea);
                return this;
            }
            //Setear cargo cuando ya existe
            public EmpleadoBuilder SetCargo(int IdCargo)
            {
                Cargo = new Cargo(IdCargo);
                return this;
            }
            //Setear y crear un cargo
            public EmpleadoBuilder SetCargo(string NombreCargo)
            {
                Cargo = new Cargo(NombreCargo);
                return this;
            }
            //Setear dirección existente
            public EmpleadoBuilder SetDireccion(int IdDireccion)
            {
                Direccion = new Direccion(IdDireccion);
                return this;
            }
            //Setear y crear nueva dirección
            public EmpleadoBuilder SetDireccion(string Domicilio, string Ciudad, string Pais, string CodigoPostal)
            {
                Direccion = new Direccion(Domicilio, Ciudad, Pais, CodigoPostal);
                return this;
            }
            //Añadir un registro telefónico existente
            public EmpleadoBuilder AddTelefonos(int IdTelefono)
            {
                Telefonos.Add(new Telefono(IdTelefono));
                return this;
            }
            //Añadir y crear teléfonos
            public EmpleadoBuilder AddTelefonos(int CodigoInternacional, int NumeroTelefonico, string TipoTelefono)
            {
                Telefonos.Add(new Telefono(CodigoInternacional, NumeroTelefonico, TipoTelefono));
                return this;
            }
            public Empleado Build()
            {
                return new Empleado(IdEmpleado, Nombre, FechaIngreso, Area, Cargo, Direccion, Telefonos);
            }
        }
    }

Interfaz IBuilder

    public interface IBuilder<T>
    {
        T Build();
    }

El uso de la interfaz es opcional, sin embargo es una buena práctica, así que te recomiendo utilizarla para poder aplicar como se debe este patrón. En adelante todas las demás entidades a las que apliquemos este patrón implementarán esta interfaz en sus respectivas clases anidadas Builder.

Probemos en Consola:

Console.WriteLine("Builder Pattern Applied!");

//Creo un objeto empleado y me olvido de crear más objetos, simplemente seteo la información
Empleado empleado = new Empleado.EmpleadoBuilder()
    .SetId(123)
    .SetNombre("Gerson")
    .SetFechaIngreso(new DateTime(2020,12,20))
    .SetArea("IT")
    .SetCargo(".NET Engineer")
    .SetDireccion("Calle Begonias 23","Lima","Peru","54321")
    .AddTelefonos(51,789545521,"Móvil")
    .AddTelefonos(1,985552000,"Whatsapp")
    .AddTelefonos(51,8526320,"Fijo")
    .Build();

Console.WriteLine(empleado);

En pantalla tenemos:

El objeto y sus dependencias se han creado satisfactoriamente, sin necesidad que el programador esté preocupado de crear objetos dependientes ni asignarlos manualmente 🥳🔥

Es momento que dejes el código y te vayas a jugar tenis crack 🎾😉 Foto de Chino Rocha en Unsplash

Eso es todo crack! Ahora te toca practicarlo y aplicarlo en tus proyectos.

Este y muchos otros artículos referidos a Patrones de diseño en la vida real los puedes encontrar en la categoría Patrones de diseño! y en este repo de mi cuenta Github. 🐿️

Por supuesto si te ha gustado esta entrada, compártela con tu equipo de ingeniería genio! 🐿️

Créditos foto de portada: Foto de Jacek Dylag en Unsplash

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *