Arquitectura hexagonal: introducción y estructura

Aplicar una buena arquitectura de diseño en nuestros proyectos es fundamental para conseguir que éstos posean una base sólida, escalable y adaptable a futuros cambios. La arquitectura hexagonal nos ayuda a alcanzar ese objetivo. 

Arquitectura de software: definición e importancia 

En el área del desarrollo nos enfrentamos a sistemas cada vez más complejos que requieren de una estructura sólida que nos facilite su creación, mantenimiento y crecimiento en el futuro. Debido a esto, el concepto de arquitectura en el ámbito del software adquiere una importancia cada vez mayor. 

La arquitectura de software establece un conjunto de marcos definidos y claros para interactuar con el código fuente además de definir, de una manera abstracta, el conjunto de componentes, sus interfaces y la comunicación entre ellos.

Esta arquitectura se establece en base a unos objetivos, no solamente funcionales, sino también otros tales como mantenimiento, testeabilidad, reutilización, flexibilidad (ante cambios) e independencia de otros sistemas.

Como ejemplos de arquitecturas podemos encontrar: Modelo-Vista-Controlador, cliente-servidor, orientada a servicios (SOA), dirigida por eventos, arquitectura por capas o arquitectura hexagonal, entre otros. 

Introducción a la Arquitectura Hexagonal 

En el año 2005 Alistair Cockburn publicó un artículo donde describía que la intención de la Arquitectura Hexagonal era la de permitir que una aplicación sea usada de la misma forma por usuarios, programas, pruebas automatizadas y scripts, y que pudiera ser tanto desarrollada como probada de forma aislada de sus eventuales dispositivos y bases de datos en tiempo de ejecución. 

Esta arquitectura, también llamada arquitectura de puertos y adaptadores propone separar nuestra aplicación en distintas capas o regiones, cada una de ellas con su propia responsabilidad permitiendo así que evolucionen de manera aislada, y que cada una de ellas sea testeable e independiente de las demás. 

Para conseguir esta independencia de capas se utiliza el concepto de puertos y adaptadores. Un puerto no es más que un concepto lógico mediante el cual se define un punto de entrada y salida de la aplicación. La función del adaptador es la de implementar la conexión con ese puerto y otros servicios externos. De esta forma podremos tener múltiples adaptadores para un mismo puerto. Por ejemplo, nuestro framework adaptará un puerto SQL por cada número de servidores de base de datos diferentes que nuestra aplicación pueda usar. 

Existen dos clases de puertos y adaptadores: primarios secundarios. La diferencia entre ellos radica en quién desencadena o está a cargo de la conversación. 

En el caso de los puertos y adaptadores primarios, es el usuario quien, mediante la interfaz de usuario, realiza una solicitud a la aplicación. Por ejemplo, a través de una petición HTTP un usuario puede solicitar un listado. Estos puertos y adaptadores pueden apreciarse en la parte izquierda del diagrama hexagonal. 

Por otro lado, en los puertos y adaptadores secundarios, la acción es desencadenada por la aplicación. Por ejemplo, podría realizarse una solicitud de persistencia en base de datos proveniente de una acción de un adaptador primario. Estos casos se muestran en la parte derecha del hexágono. 

La forma de hexágono no tiene nada que ver con su número de lados sino más bien con su representación ya que cada lado representa un puerto hacia dentro o hacia fuera de la aplicación. 

Estructura de capas 

La arquitectura hexagonal propone describir la aplicación en varias capas. El motivo es conseguir una división conceptual de las diferentes áreas de la aplicación. El código de cada capa describiría cómo comunicarse con las demás usando interfaces (puertos) e implementaciones (adaptadores). 

De este modo tendremos una aplicación que será:

  • Independiente de frameworks: el proyecto nunca dependerá de ningún framework externo ya que siempre existirá una capa que abstrae la lógica, y hará posible cambiar de framework sin afectar a la aplicación. 
  • Testeable: la lógica de negocio se podrá testear independientemente de la interfaz y de los agentes externos. 
  • Independiente de UI: el sistema no dependerá de la interfaz gráfica y ésta podrá ser modificable sin que afecte a la lógica de negocio de la aplicación. 
  • Independiente de la base de datos: el dominio de la aplicación no conocerá cómo la información es estructurada y guardada en un repositorio.  
  • Independiente de agentes externos: las reglas de negocio no tienen conocimiento de la existencia de ningún agente externo. Tan solo necesitan saber qué requieren dichos agentes para realizar sus tareas. 

Las capas en las que podemos dividir nuestro sistema aplicando esta arquitectura son: 

1. Capa de dominio 

Es la capa central del hexágono y contiene las reglas de negocio. En ella podemos encontrar los modelos de datos y sus restricciones.  

Esta capa no conoce cómo se estructurará, se guardará y se recuperará la información del repositorio. Simplemente expondrá una serie de interfaces (puertos) que serán adaptados en la capa de infraestructura para cada caso concreto de implementación de esa persistencia. 

2. Capa de aplicación 

Justo por encima de la capa de dominio encontramos la capa de aplicación, en la que se definen sus distintos casos de uso. La definición de los casos de uso se hace pensando en las interfaces disponibles en el hexágono de la aplicación y no en alguna de las tecnologías disponibles que podamos utilizar. 

En esta capa también se adaptan las distintas peticiones que recibe la aplicación a través desde la capa de infraestructura. Por ejemplo, un caso de uso aceptará los datos de entrada que provienen de la capa de infraestructura y realizará las acciones necesarias para devolverle unos datos de salida. 

3. Capa de infraestructura 

Esta es la capa más exterior del hexágono y corresponde a las implementaciones o adaptaciones de las interfaces o puertos de las demás capas.  

Normalmente esta capa corresponde al framework pero también contiene librerías de terceros, SDKs o cualquier otro código externo a la aplicación.  

La capa de infraestructura implementa los servicios definidos en la capa de aplicación (adaptadores secundarios). Por ejemplo, si tenemos definido en ella un servicio para enviar emails o SMS, en esta capa implementaremos ese servicio de acuerdo a los requisitos del proveedor o cualquier librería externa. 

Además, esta capa contiene todo lo relativo a la interacción con el usuario (adaptadores primarios). Obtendrá unos datos de entrada que serán utilizados para solicitar el caso de uso correspondiente en la aplicación y devolverá unos datos de salida. Aquí, podemos encontrar los controladores HTTP o scripts de línea de comandos, entre otros. 

4. Comunicación entre capas 

Tal y como hemos comentado, cada capa debe definir una serie de puertos que serán adaptados para cada implementación concreta. Estos puertos son las interfaces de clases que definen cómo cada capa externa puede comunicarse con la capa actual.  

Para conseguirlo, haremos uso de la inyección de dependencias, inyectando las dependencias en la clase en vez de instanciarlas dentro. De esta forma tendremos las clases de las demás capas desacopladas haciendo que dependan de una interfaz, en vez de una implementación concreta. 

Así, lo que estamos consiguiendo es invertir el control de la aplicación, evitando que nuestro programa dependa de una tecnología en concreto, permitiendo que sea la tecnología la que se adapte a los requisitos de la aplicación. 

Conclusión 

En WATA Factory hemos apostado por este patrón de arquitectura en algunos de nuestros proyectos más grandes. Nos permite lograr un aislamiento de cada una de las capas, nos proporciona flexibilidad a la hora de realizar cualquier cambio de infraestructura o de algún servicio externo, testear fácilmente y, sobre todo, nos ayuda a aplicar SOLID, obteniendo un código más limpio y mantenible en el tiempo.