Design Patterns - Composite

TLDR: El Composite es un patrón de diseño estructural que te permite componer objetos en una estructura de arbol y despues trabajar con esas estructuras como si fuera un objeto individual.


El Composite es un patrón de diseño estructural que te permite componer objetos cuya estructura es de arbol para representar jerarquías parciales. Con este patrón se ayuda al cliente tratar los objetos individualmente y componer objetos uniformemente.

¿Porque necesitamos el Composite?

Este patrón tiene sentido cuando el modelo que se va a usar puede ser representado como un árbol. Hay veces necesitamos hacer un objeto que tenga un compendio de objetos, como una colección de cosas.
Por ejemplo:

  • Una expresión matemática que está compuesta de expresiones simples.
  • Una aplicación de dibujo, esta tiene un grupo de formas que son generadas a partir de diferentes formas.

Hay varias formas de atacar este problema, usualmente siempre podemos usar otras propiedades y campos a través de la herencia para representar este tipo de situaciones, o también, por medio del principio de composición donde simplemente se tiene referencia de otra clase y se pueden ver los métodos y campos de esta. Estas son soluciones validas para este problema, pero este patrón nos ayudará a ver el problema de una forma más simple y manejable.

El patrón de diseño Composite es usado para tratar ambos objetos individuales y de composición uniformemente. Y uniformemente se quiere decir que tienen la misma interfaz.

En definición, ¿Que es el patrón Composite?

Es un mecanismo para tratar objetos individuales (Scalar) y objetos de composición de una forma uniforme.

El problema en un ejemplo

Imaginemos que tenemos un software de logística en el que procesamos información de envíos y tenemos el siguiente escenario:

Se tiene una Caja que puede contener varios Productos, como también un número de pequeñas Cajas. Esas pequeñas Cajas adicionalmente pueden almacenar algún Producto o incluso Cajas aún más pequeñas y así sucesivamente. Si quisiéramos saber cuantos productos tenemos, tendríamos que iniciar por abrir caja por caja y contar los productos. El problema planteado se puede representar en el siguiente diagrama.

Para poder lograrlo desde un acercamiento del patrón Composite, iniciaremos crearemos una clase base llamada LogisticalPackage que será nuestra interfaz.

class LogisticalPackage {
  Operation() {
    /* abstract */
  }
  Add(Component) {
    /* abstract */
  }
  Remove(Component) {
    /* abstract */
  }
  GetChild(key) {
    /* abstract */
  }
}

Ahora que tenemos nuestra clase base lista, crearemos 2 subclases: El producto que se llamará Item y el contenedor llamado BoxContainer, como se puede ver a continuación.

class BoxContainer extends LogisticalPackage {
  constructor(name) {
    super();
    this.name = name;
    this.children = [];
    console.log(`Creada un nuevo BoxContainer: ${this.name}`);
  }

  Operation() {
    console.log("Ejecutando las operaciones de BoxContainer: " + this.name);
    for (var i in this.children) this.children[i].Operation();
  }

  Add(Component) {
    this.children.push(Component);
  }

  Remove(Component) {
    for (var i in this.children)
      if (this.children[i] === Component) this.children.splice(i, 1);
  }

  GetChild(key) {
    let child = this.children[key];
    console.log(`${this.name}: Obteniendo hijos ${child}`);
    return child;
  }
}

Para las clases de tipo contenedor, una de las cosas que podemos notar de esta clase BoxContainer es que hemos agregado los métodos Operation() Add() Remove() y GetChild(). La idea principal con esta clase es poder proveer un espacio para agregar mas objetos, ya sean de tipo Producto o más Cajas. Ahora le daremos un vistazo a las funcionalidades que se implementarán en estos métodos:

  • Add: Es el encargado de agregar objetos de tipo LogisticalPackage a la lista this.children.
  • Remove: Es el encargado de eliminar en algún objeto a partir de una comparación simple.
  • Operation: Aquí es donde se ejecuta las operaciones necesarias de la lógica de negocio. Como esta implementación es de tipo contenedor, se realiza una iteración de la lista this.children (previamente llenada por el método add()) y se ejecutarán todas las operaciones de los hijos.
  • GetChild: Esta es una utilidad para obtener un objeto en especifico por medio de un key.

En el caso de la clase Item, la implementación es ligeramente diferente debido a que será el último nivel en la jerarquía, o la Hoja en el árbol.

class Item extends LogisticalPackage {
  constructor(name, price) {
    super();
    this.name = name;
    this.price = price;
    console.log(`Creado nuevo articulo ${this.name} $${this.price}`);
  }

  Operation() {
    console.log(
      `Ejecutando Operación para el Item: ${this.name} $${this.price}`
    );
    return this.price;
  }
}

Listo! Ya tenemos nuestras clases básicas. Aprenderemos como usarlas:

let caja1 = new BoxContainer("Caja de Celular");
// Creada un nuevo BoxContainer: Caja de Celular
caja1.Add(new Item("Celular", 800));
// Creado nuevo articulo Celular $800
caja1.Add(new Item("Audifonos", 100));
// Creado nuevo articulo Audifonos $100

let caja2 = new BoxContainer("Caja de Accesorios");
// Creada un nuevo BoxContainer: Caja de Accesorios
caja2.Add(new Item("Cargador Celular", 80));
// Creado nuevo articulo Cargador Celular $80
caja2.Add(new Item("Powerbank", 200));
// Creado nuevo articulo Powerbank $200

let cajaPrincipal = new BoxContainer("Envio #12345");
// Creada un nuevo BoxContainer: Envio #12345

cajaPrincipal.Add(caja1);
cajaPrincipal.Add(caja2);

Ahora ejecutaremos la función principal que nos permitirá mostrar su estructura interna.

cajaPrincipal.Operation();
// Ejecutando las operaciones de BoxContainer: Envio #12345
//   Ejecutando las operaciones de BoxContainer: Caja de Celular
//      Ejecutando Operación para el Item: Celular $800
//      Ejecutando Operación para el Item: Audifonos $100
//   Ejecutando las operaciones de BoxContainer: Caja de Accesorios
//      Ejecutando Operación para el Item: Cargador Celular $80
//      Ejecutando Operación para el Item: Powerbank $200

Ahora como podemos ver, solamente ejecutando la función Operation() de el objeto principal, podremos ver el resultado de todos los items que pertenecen al árbol de una forma fácil de entender.

Código completo del ejemplo: https://bit.ly/3cIrIvm

Estructura del patrón Composite

En este patrón encontramos varios componentes que interactuaran entre si.

  1. El Component, que en nuestro ejemplo es la clase LogisticalPackage, es la interfaz que se encarga de tener el método que será implementado en todos los hijos.
  2. Las Leaf o hojas, que en nuestro ejemplo es la clase Item, son los elementos básicos de un árbol. Las Hojas no tendrán sub elementos, así que estas contendrán toda la información relevante para la operación. En nuestro caso los Item son el producto en nuestro ejemplo y tienen toda la información necesaria para ejecutar las operaciones.
  3. El Container o llamado Composite, que en nuestro ejemplo es el BoxContainer, es el elemento que contendrá los sub-elementos; estos pueden ser Hojas o otros containers. Este contenedor no sabe cual será la clase en concreto que tendrá sus hijos, pero se usará todos los sub-elementos asociados a su interface en común, LogisticalPackage.
    El contenedor al recibir la petición de uso, delega el trabajo a todos sus sub-elementos como fue el caso de la función Operation() del BoxContainer.
  4. En esta ultima etapa está el Client que es el encargado de trabajar con todos los elementos. Como resultado, el cliente podrá trabajar en la misma forma con elementos simples o complejos de un arbol.

Conclusiones

  • El patrón de diseño Composite te permite tratar tanto los objetos individuales como los objetos de composición de una manera uniforme, permitiéndonos aplicar el mismo comportamiento independientemente de si estamos trabajando con uno o más elementos.
  • En Composite, los dos tipos de elementos básicos comparten una interfaz en común para ayudarte a manejar estructuras de objeto tipo arbol.
  • Con este patrón va a ser más fácil introducir nuevos tipos como Item dentro de una aplicación sin romper el código existente, aunque muchas veces vas a tener que generalizar la interfaz. Esto permite que se respete el principio de Open/Closed Principle.

Ejercicio

Se requiere que se construya una solución para el manejo de una estructura empresarial como la siguiente:

Se requiere crear una solución aplicando el patrón Composite, para que todos los empleados de la empresa se puedan presentar.

Estructura Inicial

Solución esperada

Recursos adicionales y bibliografia

Alexander Shvets - Dive into design Patterns

Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides - Design Patterns Elements of Reusable Object-Oriented Software (1994)

Addy Osmani - Learning JavaScript Design Patterns (2012)

https://www.dofactory.com/javascript/composite-design-pattern

https://loredanacirstea.github.io/es6-design-patterns/#composite