Algunas dudas comunes con Arquitectura Hexagonal

por Fran Iglesias

Antes de pasar a desarrollar un ejemplo de aplicación “Hexagonal”, voy a tratar de responder a algunas dudas comunes que se plantean recurrentemente. O que veo recurrentemente mal respondidas en algunas referencias.

¿Usamos capas en Arquitectura Hexagonal?

La arquitectura hexagonal no prescribe ninguna organización del código. Específicamente no postula ninguna estructura en capas. Esto no implica que la aplicación sea un totum revolutum, pero nos deja libertad en cuanto a cómo queremos estructurarla.

Específicamente, la famosa división entre Dominio, Aplicación e Infraestructura, no está ligada de ningún modo a Arquitectura Hexagonal, sino que proviene de otros modelos como Onion o Clean Architecture.

Como nota al margen: hablar de Dominio tampoco implica que estemos haciendo Domain Driven Design. El dominio de una aplicación se refiere, en general, al pedazo de mundo que modela y los problemas que nos ayuda a abordar. En Arquitectura Hexagonal, lo que llamamos Dominio es el hexágono1 de la aplicación.

En mi opinión, el criterio organizativo interno de la aplicación tiene que basarse más en los casos de uso, o sea: en las conversaciones iniciadas por los driving actors, que son quienes accionan el sistema porque quieren conseguir un resultado. Esta idea tiene ciertas similitudes con la llamada Vertical Slice Architecture en tanto que el software se organiza a partir de prestaciones que ofrece el sistema y que atraviesan todas sus capas.

A priori, la parte que necesita más organización es el exterior del hexágono. Un punto de partida puede ser organizar el código basándonos en los distintos puertos. Y, dentro de cada puerto, divisiones según los distintos adaptadores que necesitemos implementar.

¿Los puertos siempre son interfaces?

A esta pregunta se podría responder sí y no, a la vez. Vamos a intentar explicarlo.

Driven ports

Los puertos secundarios o Driven Ports se definen mediante interfaces. La aplicación pide a los adaptadores que la implementen a fin de poder comunicarse con las tecnologías concretas y que sean configurables aplicando el patrón de inversión de control, o inversión de dependencias.

Además, el puerto incluye los objetos que se pueden pasar entre la aplicación y el adaptador.

El ejemplo típico es el de un puerto para persistir entidades, que se expresa mediante una interfaz y que se puede implementar usando adaptadores a distintas tecnologías.

Driver ports

Los puertos primarios o Driving Ports, por su parte, han de ser usados por el adaptador. Por tanto, son concretos. Se trata de aplicaciones del patrón Command (o Query) que el adaptador tendrá que invocar, bien directamente, bien a través de un bus de commands/queries. El puerto es el objeto Comando o Query y, según el caso, su handler correspondiente o el bus de comandos/queries.

Dicho esto, tiene todo el sentido del mundo definir una interfaz aunque solo vaya a tener esa implementación. La razón es que eso nos permite testear los adaptadores independientemente de la aplicación. Haber definido la interfaz nos habilita para crear dobles de test de la aplicación y hacer stubs de sus posibles respuestas.

Además, los puertos nos interesan en tanto a que conforman la interfaz de la aplicación con el resto del mundo.

¿Un puerto una única interfaz?

Pese a todo, la definición de lo que abarca un puerto tiene cierta laxitud. Es decir, podríamos definir un puerto por cada caso de uso de la aplicación y podríamos agrupar varios casos de uso en único puerto.

En general, lo más adecuado es pensar en qué conversación define cada puerto. Posiblemente, esto nos llevará a tener puertos que involucran varios casos de uso estrechamente relacionados. Por tanto, un puerto puede estar compuesto de varias interfaces.

En resumen, los puertos definen la interfaz pública de la aplicación. Lo normal es que se definan empleando interfaces. Los adaptadores para driving ports usan esas interfaces y normalmente solo tendrán una implementación. Los adaptadores para driven ports las implementan.

Los tests ¿actores o adaptadores?

En una conversación con Juan Manuel Garrido a raíz del artículo anterior salió este tema a colación y me parece interesante comentarlo.

Los tests son actores, porque tienen comportamiento y tienen metas. Pueden iniciar una conversación con la aplicación con una finalidad. A diferencia de otros actores, los tests ya son código por lo que pueden controlar la aplicación directamente, sin necesidad de adaptadores.

Como organizar la aplicación

Hay tres grandes elementos que considerar

  • La aplicación en sí (el hexágono), que contiene el modelo de dominio y los puertos.
  • Los adaptadores, que implementan o usan los puertos y utilizan tecnologías del mundo real.
  • El sistema de arranque o bootstrap, que monta la aplicación con los adaptadores adecuados al entorno en que se va a ejecutar

¿Cómo paquetizar un proyecto?

Para paquetizar el proyecto dependeremos un poco del lenguaje en concreto en que vayamos a trabajar. Por ejemplo, Juan M. Garrido propone una paquetización para Java basada en modulos recogiendo prácticas comunes en ese lenguaje. De aquí extraeré algunas ideas, aunque el patrón de Arquitectura Hexagonal tampoco prescribe nada al respecto.

Así que intentaremos ver unos pocos principios generales que podamos aplicar. Vuelvo a señalar que esta organización no está prescrita por el patrón, sino que es un desarrollo personal basado en ejemplos y artículos que he leído.

Un paquete para la aplicación o hexágono. Dentro de este tendríamos un área para los driver ports, otra área para los driven y otra área para el código de la aplicación. Como hemos mencionado en otro momento, el nombre de los ports usaría el patrón “forDoingSomething”. El código de aplicación puede estructurarse como mejor nos parezca, aunque creo que hacerlo teniendo en cuanta las necesidades de los casos de uso es el camino a seguir.

Un paquete para cada adaptador. Para no inundar la raíz del proyecto, los paquetes de los adaptadores podrían seguir el patrón Adapter.Port.Technology.

Un paquete para el bootstrap. Este paquete incluiría el código necesario para armar la aplicación con los adaptadores adecuados según el entorno de ejecución. Como ejemplo básico: producción y tests. Pero podríamos tener una para dev, otra para staging, para QA o para cualquier otro entorno que sea significativo en nuestro contexto y requiera distintas tecnologías.

Esto nos podría dejar con una estructura similar a esta:

Project
   App [Hexagon]
     Driver [Port]
       ForDoingSomething
         Command  [DTO]
         Handler  [Command]
         Response [DTO]
     Driven
       ForDoingAnotherThing
          ThingRepository [Interface]
     Domain
   Adapters
     ForDoingSomething
        Test [Test invoking ForDoingSomethingCommand w/ Handler]
        Cli  [Command line tool that invokes the Command]
        WebUI [FE and Controllers that invoke the Command]
     ForDoingAnotherThing
        InMemoryThingRepository [Used for tests, implements ThingRepository]
        MySQLThingRepository [Production, implements ThingRepository]
   SetUp

Hagamos un ejemplo

El próximo artículo de esta serie nos presentará un ejemplo de aplicación que escribiremos usando el patrón de Arquitectura Hexagonal, aplicando todas estas ideas.

  1. Y ya que estamos, tampoco es que tengamos que tener una carpeta hexágono. Arquitectura Hexagonal no prescribe muchas cosas acerca de la estructura física del código. 

June 4, 2023

Etiquetas: design-patterns   hexagonal  

Temas

good-practices

refactoring

php

testing

tdd

python

design-patterns

blogtober19

design-principles

tb-list

misc

bdd

legacy

golang

dungeon

ruby

tools

tips

hexagonal

ddd

bbdd

soft-skills

books

oop

javascript

api

sql

ethics

typescript

swift

java

agile