Patrón Compuesto

Para el patrón compuesto vamos a utilizar el ejemplo del post anterior. Repasemos el caso de uso rápidamente. Nuestra aplicación se encarga de almacenar diferentes tipos de documentos. Por medio del Patrón Iterador, logramos simplificar la forma en que nuestro servicio recorre los diferentes tipos de archivos.

Además, encapsulamos la forma en que nuestros clientes enviaban esos archivos, por ejemplo, un cliente utilizaba un ArrayList, otro un arreglo y el último un HashMap. Para nuestro servicio, esas estructuras son indiferentes, el servicio sólo se encarga de almacenar los archivos sin importar como estén siendo enviados por los clientes.

Ahora bien, resulta que un cliente nuevo decide enviarnos una serie de directorios y desea utilizar nuestro servicio para almacenar todos los archivos contenidos dentro de ellos.

Podríamos decirle a ese cliente que para utilizar nuestro servicio, es necesario que nos envíe los archivos por separado. Claro, después de eso podemos despedirnos de ese cliente, ya que es más fácil buscar otro proveedor que sí acepte directorios.

Una vez más tenemos a nuestro amigo el cambio. Pero esta vez no es un cambio pequeño, es un cambio que implica un rediseño de nuestra aplicación actual.

El objetivo de nuestra aplicación es almacenar archivos, no importa si nos envían los archivos individuales o una serie de directorios. Tanto los archivos como los directorios funcionan en jerarquías, también conocidas como árboles. Veamos un ejemplo.

Directorios utilizados para el ejemplo de jerarquías

En la imagen anterior tenemos el archivo llamado CompositePattern.vsdx que está ubicado en la siguiente ruta: Drive/Personal/Work/RunnablePatterns/Posts/CompositePattern. La representación del árbol se vería de la siguiente manera. (Los directorios BK y Media están en el mismo nivel que el directorio Posts).

Ejemplo de una estructura jerárquica

Para que nuestra aplicación soporte archivos y rutas de archivos es necesario homologar ambos, además de poder tratar ambos objetos de la misma manera. Para lograrlo podemos hacer uso del patrón compuesto.

Esta es la definición de este patrón según Head First Design Patterns. El patrón compuesto le permite agrupar objetos en estructuras tipo árbol para representar jerarquías. Este patrón le permite a los clientes manipular los objetos compuestos (nodos) y sencillos (hojas) de manera uniforme.

En nuestra aplicación los nodos serían los directorios, es decir un directorio puede tener otros directorios dentro o archivos. Por ejemplo el directorio Posts es un nodo, ya que tiene el directorio CompositePattern.

En el caso de las hojas, serían los archivos contenidos dentro de los directorios, como por ejemplo el archivo CompositePattern.vsdx. Las hojas no tienen archivos o directorios dentro de ellos.

Con esta estructura, podemos recorrer todo el árbol (directorios) y almacenar únicamente las hojas (archivos). En un futuro, si alguno de nuestros clientes decide comenzar a enviar directorios con archivos a almacenar ya vamos a tener la lógica necesaria para poder hacerlo.

La ventaja de este patrón es que podemos realizar cualquier operación sobre todo el árbol, sin necesidad de tener que diferenciar entre nodos y hojas.

Este es el diagrama de clases de este patrón.

Diagrama de clases propuesto para el patrón compuesto

Client: Utiliza la interfaz (puede ser una interfaz o una clase abstracta) para trabajar con los objetos del árbol.

Component: Es la interfaz que agrupa los objetos del árbol, es decir, los nodos (Composite) y las hojas (Leaf). En la mayoría de los casos, se definen comportamientos por defecto para todos los métodos, ya que algunos métodos no aplican para las hojas y algunos no aplican para los nodos.

Composite: Es el encargado de administrar las operaciones que pueden realizarse sobre nodos y también sobre las hojas dentro de ellos. En nuestro caso, serían los directorios que contienen otros directorios o archivos.

Leaf: Son los elementos más simples o individuales, es decir no poseen hijos dentro de ellos, en nuestro caso, las hojas son los archivos como tal.

Al aplicar este patrón a nuestro ejemplo, el siguiente diagrama es el resultado.

Diagrama de clases del ejemplo utilizando el patrón compuesto

Revisemos las clases que hemos agregado.

  • MyDocumentComponent: Esta clase abstracta define todas las operaciones que se pueden realizar sobre el árbol, este sería nuestro Componente (Component).
  • MyDocument: Esta clase representa las hojas (Leaf), es decir, los archivos que se pueden almacenar en nuestra base de datos. Aquí solo sobre escribimos los métodos que tienen sentido para los archivos, por ejemplo getData() que nos da acceso al arreglo de bytes del archivo. Los otros métodos como add() no pertenecen a las hojas, por lo cual dejamos la implementación por defecto definida en MyDocumentComponent.
  • MyPath: Esta clase representa los nodos (Composite), es decir, los directorios. Dentro de los directorios pueden existir directorios adicionales o archivos (MyDocument). Sin embargo, para implementar el patrón correctamente, definimos el ArrayList del tipo MyDocumentComponent, para así poder almacenar directorios o archivos.
  • NullIterator: Esta clase nos define la forma de iterar en las hojas. Como las hojas no tienen elementos que recorrer, utilizamos esta clase utilitaria que a su vez es otro patrón de diseño llamado patrón Nulo. En resumen, nos permite definir un comportamiento por defecto cuando queremos evitar validaciones de valores nulos. Con este patrón evitamos excepciones como NullPointerException.
  • MyPathIterator: Esta clase nos brinda la posibilidad de recorrer todo el árbol. Al retornar un Iterador de tipo MyDocumentComponent, podemos crear la lógica para recorrer cada nodo y llegar hasta las hojas. Por ejemplo, podemos crear un método que solo se encargue de almacenar los archivos de tipo PDF e ignorar el resto de archivos.

Esta vez tenemos tres clientes diferentes:

  • ClientA: Envía sus archivos en formato PNG.
  • ClientB: Envía sus archivos en formato PDF.
  • ClientC: Envía tres directorios diferentes con los siguientes archivos:
    • pathA: Contiene dos archivos.
    • pathB: Contiene dos archivos.
    • pathC: Contiene tres archivos.

El resultado de ejecutar el código de ejemplo es el siguiente.

Al llamar solamente a saveDocuments():

Resultado de ejecutar el código de ejemplo

Al llamar solamente al método saveByType(“PDF”) que filtra el tipo de documento solo para PDFs:

Resultado de ejecutar el código de ejemplo

El código de ejemplo del patrón compuesto lo pueden descargar aquí.

En el próximo post vamos a discutir el Patrón llamado Estado.

Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.

Referencias:
Libro Head First Design Patterns. Versión digital.

Leave a Reply

Your email address will not be published. Required fields are marked *