Patrones de diseño – Composite

El patrón Composite es un patrón estructural y su objetivo es que el consumidor no sepa si está hablando con un solo objeto o si detrás hay mil objetos de ese tipo.

No lo confundáis con el Facade, el Facade nos facilita, o nos abstrae, de comunicarnos con varios sistemas para hacer una sola acción. Pero con el Composite vamos a hablar con un objeto para que haga su acción y no nos importa si este por debajo tiene otros 20 objetos del mismo tipo.

Muy a groso modo es como ocultar una lista, pero no siempre vamos a necesitar una lista y no queremos crear una lista cuando solo vamos a tener 1 elemento.

Diagrama de un Composite pattern

Lo primero de todo voy a daros una definición un poco más concreta y algunos ejemplos de uso.

Imaginemos el supuesto de que tenemos un consumidor, que consume un objeto, y ahora queremos que realmente pueda consumir varios objetos pero que no se tenga que preocupar de nada, le da igual que sea 1 que mil.

Composite

Podemos cambiar el consumidor para que en lugar de utilizar un objeto utilice una lista, pero es un poco overkill, no cumple Open-Close y la mayoría de las veces la lista tendrá un único elemento.

pattern

Vale, vamos a poner un ejemplo más concreto, ¿recordáis el patrón Command y la cola de comandos que vimos?

A modo de super resumen, un Command básicamente es una clase que encapsula una acción, mostrar fade, cargar escena, ocultar UI, hacer daño a un enemigo, en general acciones atómicas.

La estructura que tendríamos sería esta:

composite unity

Tenemos varios comandos concretos que hacen su acción concreta, pero supongamos que ahora queremos hacer un comando más complejo: queremos que cuando se dañe a un personaje se haga un camera shake por ejemplo, que se actualice la UI y lo que sea, y queremos que empiece a hacerlo todo al mismo tiempo.

Todas estas acciones ya tienen su comando, podríamos crear un nuevo comando y copiar lo que hace cada acción. Pero aunque hicieramos esto nuestra cola de comandos no permite ejecutar comandos a la vez, los ejecuta uno detrás de otro.

Otra opción sería utilizar la fantástica técnica de composición y aplicar un patrón composite para ampliar nuestro sistema sin tener que cambiar absolutamente nada de lo existente.

Lo que tendriamos sería algo como este diagrama:

composite pattern unity

El CompositeCommand contendría una lista de ICommand que queremos ejecutar al mismo tiempo, utilizaríamos el AddCommand para añadir un comando nuevo y el execute simplemente los ejecutaría todos a la vez y se esperaría a que todos terminasen.

Implementar un patrón Composite con Unity

Antes de empezar con el patrón en si vamos a dar un vistazo a la cola de comandos, aunque puedes encontrar una descripción más detallada en el post de Command pattern.

public class CommandQueue
    {
        public static CommandQueue Instance => _instance ?? (_instance = new CommandQueue());

        private readonly Queue<ICommand> _commandsToExecute;
        private bool _runningCommand;
        private static CommandQueue _instance;

        private CommandQueue()
        {
            _commandsToExecute = new Queue<ICommand>();
            _runningCommand = false;
        }
        
        public void AddCommand(ICommand commandToEnqueue)
        {
            _commandsToExecute.Enqueue(commandToEnqueue);
            RunNextCommand().WrapErrors();
        }

        private async Task RunNextCommand()
        {
            if (_runningCommand)
            {
                return;
            }
            
            while (_commandsToExecute.Count > 0)
            {
                _runningCommand = true;
                var commandToExecute = _commandsToExecute.Dequeue();
                await commandToExecute.Execute();
            }

            _runningCommand = false;
        }
    }

Esta clase se encarga de encolar todos los Comandos que le lleguen y los irá ejecutando uno detrás de otro, nunca en paralelo.

Luego tenemos unos comandos concretos que son los que queremos utilizar para hacer nuestro ataque y cada uno por individual puede tardar varios frames. Para no perdernos en detalles he simplificado estos comandos para que solo muestren un log:

public class CameraShakeCommand : ICommand
    {
        public async Task Execute()
        {
            Debug.Log("Camera shakeeeee");
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
    }
public class UpdateHealthUiCommand : ICommand
    {
        public async Task Execute()
        {
            Debug.Log("Update UI");
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
    }
public class DoDamageCommand : ICommand
    {
        public async Task Execute()
        {
            Debug.Log("Damage");
            await Task.Delay(TimeSpan.FromSeconds(2));
        }
    }

Y los consumimos de la siguiente forma:

public class CombatConsumer : MonoBehaviour
    {
        private void Update()
        {
            if (Input.GetKeyUp(KeyCode.F1))
            {
                CommandQueue.Instance.AddCommand(new DoDamageCommand());
                CommandQueue.Instance.AddCommand(new CameraShakeCommand());
                CommandQueue.Instance.AddCommand(new UpdateHealthUiCommand());
            }
        }
    }

El output que obtenemos será el siguiente, fijaros en los segundos:

Command pattern log 1

Esto está bien pero tiene la pega de que a nosotros nos interesa que todos estos comandos ocurran al mismo tiempo. Pero no queremos modificar la cola de comandos porque en el resto del código si que queremos que se espere para ejecutar el siguiente comando.

Para solucionar esto podemos aplicar el patrón Composite y crear un comando que pueda contener otros comandos, como una Matrioska, y que los ejecute al mismo tiempo.

Este nuevo comando CompositeCommand, necesita un método para añadir comandos y en el Execute recorrerá la lista que tiene almacenada, ejecutará todos los comandos y esperará a que todos terminen para dar por terminado este comando.

public class CompositeCommand : ICommand
    {
        private List<ICommand> commands;

        public CompositeCommand()
        {
            commands = new List<ICommand>();
        }

        public void AddCommand(ICommand command)
        {
            commands.Add(command);
        }
        
        public async Task Execute()
        {
            var tasks = new List<Task>();
            foreach (var command in commands)
            {
                tasks.Add(command.Execute());
            }

            await Task.WhenAll(tasks);
        }
    }

Si actualizamos el consumidor para que utilice este Composite lo que obtendremos es esto:

public class CombatConsumer : MonoBehaviour
    {
        private void Update()
        {
            if (Input.GetKeyUp(KeyCode.F1))
            {
                var compositeCommand = new CompositeCommand();
                compositeCommand.AddCommand(new DoDamageCommand());
                compositeCommand.AddCommand(new CameraShakeCommand());
                compositeCommand.AddCommand(new UpdateHealthUiCommand());
                
                CommandQueue.Instance.AddCommand(compositeCommand);
            }
        }
    }

Y la salida ahora mostrará todos los logs en el mismo segundo:

Composite pattern log 2

Conclusión

Así de sencillo es un Composite, solo necesitamos una clase para añadir los componentes del mismo tipo que implementa. También podríamos añadir otros métodos para quitar y gestionar la lista si lo necesitáramos.

Además si os habéis dado cuenta, no hemos tocado para nada las clases que ya teníamos, ni el CommandQueue ni ningún Command, esto solo quiere decir una cosa, el patrón composite es un gran amigo del principio SOLID de Open-Close que como ya sabéis nos dice que una clase o sistema tiene que estar abierto a la extensión pero cerrado a la modificación.

Lo que hemos visto es solo una implementación, para mi la mejor, pero como la mayoría de patrones los podemos adaptar y tienen varias formas de implementarse.

En la implementación original este método de AddCommand lo añadiriamos en la interfaz del ICommand que en su lugar sería una clase abstracta, y en el método Add no haríamos nada en la mayoría de los casos y solo los comandos que necesiten componer harían esta gestión.

A mi esa versión no me gusta nada porque estamos publicando unos detalles que solo unos consumidores concretos necesitan conocer, la cola de comandos no necesita conocer el Add, solo el Execute.

Esa es la magia de la encapsulación, cuanto menos detalles publiquemos mejor para todos y más fácil será de utilizar nuestro sistema.

Si quieres descargar el código de ejemplo que hemos utilizado y probarlo tu mismo aquí te dejo el enlace.

Libros sobre patrones de diseño

Otras entradas

Resumen
➤ Patrones de diseño - Composite
Nombre del artículo
➤ Patrones de diseño - Composite
Descripción
El objetivo del COMPOSITE pattern es que el consumidor no sepa si está hablando con un solo objeto, o si detrás hay mil objetos de ese tipo 🤓
Autor
Publisher Name
The Power Ups - Learning
Publisher Logo