Ejemplo patrón compuesto (Composite) en C#



En este post, que es una continuación de "El patrón Comuesto (Composite) en C#",  voy a implementar un ejemplo del patrón a partir de un desarrollo que realicé para una empresa de producción industrial.

Consistía en crear un módulo para calcular los costes de productos formados por conjuntos de otros productos.

Inicié el desarrollo del módulo partiendo de la premisa Código Primero y apoyándome en el ejemplo canónico explicado en el post anterior.

Ejemplo real de los costes

Lo primero es plantear un problema, que aparentemente parece sencillo.

Una empresa fabrica ejes y engranajes a los que llaman referencias. Cada referencia tiene un coste de fabricación. Por ejemplo:

Referencia Coste
ProductoA 5 €
ProducoA1 2 €
ProductoA2 3 €
----------- ----
ProductoB 4 €
ProductoB1 1 €
ProductoB2 2 €
ProductoB21 1 €
ProductoB22 2 €

Las referencias se pueden vender por separado, pero también pueden formar conjuntos y venderse como una unidad. Por ejemplo:

CONJUNTO A
Cantidad Referencia
1 A
3 A1
2 A2
CONJUNTO B
Cantidad Referencia
1 B
5 B1
3 B2 (es un conjunto)
CONJUNTO B2
Cantidad Referencia
1 B2
4 B21
2 B22

Uno de los puntos clave del problema es que el conjunto B está formado por una referencia B2 que a su vez es un conjunto formado por otras referencias.

El módulo que me pidieron tenía que ser capaz de calcular los costes de cualquier referencia y/o conjunto.

En el fondo sería similar al problema de calcular el coste de un “carrito de la compra” de un e-commerce compuesto por productos que a su vez pueden ser conjuntos de otros productos.

Primer intento

Lo primero que me vino a la cabeza para resolver el problema es el patrón compuesto, así que lo tomé como ejemplo y empecé a desarrollar una solución. Así quedó partiendo del ejemplo canónico.

static void Main(string[] args)
{          
            var referenciaB = new Referencia("B",coste: 4);
            var conjuntoB = new Conjunto(referenciaB);

            var referenciaB1 = new Referencia("B1", coste: 1);
            var piezaB1 = new Pieza(referenciaB1);

            var referenciaB2 = new Referencia("B2", coste: 2);
            var conjuntoB2 = new Conjunto(referenciaB2);

            var referenciaB21 = new Referencia("B21", coste: 1);
            var piezaB21 = new Pieza(referenciaB21);

            var referenciaB22 = new Referencia("B22", coste: 2);
            var piezaB22 = new Pieza(referenciaB22);

            conjuntoB2.Añadir(3, piezaB21);
            conjuntoB2.Añadir(2, piezaB22);

            conjuntoB.Añadir(5, piezaB1);
            conjuntoB.Añadir(3, conjuntoB2);    

            // Hasta aquí he creado el conjuntoB
            
            // A continuación el cálculo del coste en el que solo 
            // es necesario una línea

            Console.WriteLine("Coste de " + conjuntoB.Nombre + ":");
            Console.WriteLine(conjuntoB.CalcularCoste(1));
}   

        public class Referencia
        {
            public Referencia(string nombre, decimal coste)
            {
                if (string.IsNullOrEmpty(nombre))
                    throw new ArgumentException(nameof(nombre));

                if (coste < 0)
                    throw new ArgumentException(nameof(coste));

                Nombre = nombre;
                Coste = coste;
            }
            public string Nombre { get; }
            public decimal Coste { get; }
        }

        public abstract class Componente
        {
            public abstract string Nombre { get; }

            public abstract void Añadir(Componente componente);

            public abstract void Quitar(Componente componente);

            public abstract decimal CalcularCoste(int nivel);
        }

        public class Pieza : Componente
        {
            private readonly Referencia _referencia;

            public Pieza(Referencia referencia)
            {
                _referencia = referencia;
            }

            public override string Nombre => _referencia.Nombre;

            public override void Añadir(Componente componente)
            {
                throw new NotImplementedException();
            }

            public override void Quitar(Componente componente)
            {
                throw new NotImplementedException();
            }

            public override decimal CalcularCoste(int nivel)
            {
                Console.WriteLine(new String('-', nivel) + " Pieza: " + Nombre + " - Coste:" + _referencia.Coste);

                return _referencia.Coste;
            }
        }

      public class Conjunto : Componente
        {
            private readonly Referencia _referencia;
            private readonly List<Componente> _subComponentes;


            public override string Nombre
            {
                get { return _referencia.Nombre; }
            }

            public override void Añadir(Componente componente)
            {
                _subComponentes.Add(componente);
            }

            public override void Quitar(Componente componente)
            {
                _subComponentes.Remove(componente);
            }

            public Conjunto(Referencia referencia)
            {
                _referencia = referencia;
                _subComponentes = new List<Componente≶();
            }

            public void Añadir(int cantidad, Componente componente)
            {
                for (int i = 0; i < cantidad; i++)
                {
                    Añadir(componente);
                }
            }


            public override decimal CalcularCoste(int nivel)
            {
                decimal coste = _referencia.Coste;

                Console.WriteLine(new String('-', nivel) + " " + Nombre + ": " + coste);

                foreach (var componenteProducto in _subComponentes)
                {
                    coste = coste + componenteProducto.CalcularCoste(nivel + 1);
                }

                return coste;
            }
        }


En este primer intento parto de una clase abstracta componente y luego implemento la clase pieza y la clase conjunto.

Este es el resultado:

Segundo intento

Hasta aquí, seguí las instrucciones del patrón al pie de la letra. Pero repasando el código vi opciones para simplificarlo. En este caso particular, ¿es necesario definir una clase para conjunto y otra para pieza?

Hablando con los futuros usuarios del módulo me comentaron que cualquier referencia se puede vender por separado y además en cualquier momento puede formar un conjunto y venderlo como tal.

Es parecido a pensar que una hoja de un árbol en el futuro puede convertirse en una rama con otras hojas.

Pues aunque no lo parezca este comportamiento simplificó la implementación. En la práctica solo necesitaba una sola clase Componente. No es necesario saber cuándo se trata de una pieza o de un conjunto:

static void Main(string[] args)
{     
   
            var referenciaB = new Referencia("B", 4);
            var conjuntoB = new Componente(referenciaB);

            var referenciaB1 = new Referencia("B1", 1);
            var piezaB1 = new Componente(referenciaB1);

            var referenciaB2 = new Referencia("B2", 2);
            var conjuntoB2 = new Componente(referenciaB2);

            var referenciaB21 = new Referencia("B21", 1);
            var piezaB21 = new Componente(referenciaB21);

            var referenciaB22 = new Referencia("B22", 2);
            var piezaB22 = new Componente(referenciaB22);

            conjuntoB2.Añadir(3, piezaB21);
            conjuntoB2.Añadir(2, piezaB22);

            conjuntoB.Añadir(5, piezaB1);
            conjuntoB.Añadir(3, conjuntoB2);

            Console.WriteLine("Coste de " + conjuntoB.Nombre + ":");
            Console.WriteLine(conjuntoB.CalcularCoste(1));
        
   }

        public class Componente
        {
            private readonly Referencia _referencia;
            private readonly List<Componente> _subComponentes;


            public string Nombre
            {
                get { return _referencia.Nombre; }
            }

            public void Añadir(Componente componente)
            {
                _subComponentes.Add(componente);
            }

            public void Quitar(Componente componente)
            {
                _subComponentes.Remove(componente);
            }

            public Componente(Referencia referencia)
            {
                _referencia = referencia;
                _subComponentes = new List<Componente>();
            }

            public void Añadir(int cantidad, Componente componente)
            {
                for (int i = 0; i < cantidad; i++)
                {
                    Añadir(componente);
                }
            }


            public decimal CalcularCoste(int nivel)
            {
                decimal coste = _referencia.Coste;

                Console.WriteLine(new String('-', nivel) + " " + Nombre + ": " + coste);

                foreach (var componenteProducto in _subComponentes)
                {
                    coste = coste + componenteProducto.CalcularCoste(nivel + 1);
                }

                return coste;
            }
        }

Esto simplificó el código, sin embargo, me había alejado de la definición canónica del "Patrón Compuesto". Quizá ya no se trataba del mismo patrón pero dado que la esencia y el objetivo prevalecen, para mí lo sigue siendo. Este es el resultado:

Tercer intento

Desde un punto de vista de código ya me quedé satisfecho con la implementación anterior, pero cuando empecé a pensar en cómo se iba a guardar esta estructura en base de datos me di cuenta que quizá no era del todo eficiente.

Con la estructura del segundo intento, si un producto A se compone de 100 productos A1, se van a crear 100 registros iguales en la base de datos. Quizá sea más eficiente crear un solo registro y añadir una columna que indique el total de componentes del mismo producto.

Por comodidad suelo trabajar con Entity Framework como estrategia para interactuar con la base de datos. La representación de la columna cantidad en la tabla se traduce en una nueva propiedad en la clase Componente:

        public class Componente
        {
            private readonly Referencia _referencia;
            private readonly List<Componente> _subComponentes;

            public int Cantidad { get; set; }

            public string Nombre
            {
                get { return _referencia.Nombre; }
            }

            public void Añadir(Componente componente)
            {
                _subComponentes.Add(componente);
            }

            public void Quitar(Componente componente)
            {
                _subComponentes.Remove(componente);
            }

            public Componente(Referencia referencia)
            {
                _referencia = referencia;
                _subComponentes = new List<Componente>();
                Cantidad = 1;
            }

            public void Añadir(int cantidad, Componente componente)
            {
                componente.Cantidad = cantidad;
                Añadir(componente);
                
            }


            public decimal CalcularCoste(int nivel)
            {
                decimal coste = _referencia.Coste;

                Console.WriteLine(new String('-', nivel) + $" {Nombre} - Coste: {coste} - Cantidad: {Cantidad}");

                foreach (var componenteProducto in _subComponentes)
                {
                    coste = coste + componenteProducto.CalcularCoste(nivel + 1) 
                                    * componenteProducto.Cantidad;
                }

                return coste;
            }
        }

Este es el resultado que se obtiene:

La implementación del tercer intento fue con la que me quedé finalmente. A dia de hoy está funcionando y no ha necesitado más retoques.

Código en GitHub




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