Desarrollo ágil con Arquitectura limpia Hexa3

Dario Palminio
9 min readSep 30, 2021

--

¿Cómo podemos usar arquitectura limpia para desarrollar ecosistemas de aplicaciones distribuidas con equipos ágiles?

Una forma es usando arquitetura Hexa3, que incluye arquitectura hexagonal y DDD.

Patrón de tres capas

Antes de contestar esta pregunta anterior, podemos comenzar por… ¿Cómo podemos implementar una arquitectura limpia?

Una manera de ir en este camino es usar un patrón de arquitectura de software basada en capas. El “patrón de capas” (Layers Pattern) se centra en una abstracción y cohesión jerárquica de los roles y responsabilidades, proporcionando una separación efectiva de las preocupaciones (cada espacio de módulos o componentes en conjunto se encarga de lo que le corresponde). Es una manera de implementar el principio de diseño: “Separation of Concerns” (SoC). Bajo este patrón se han propuesto diferentes niveles de capas de abstracción.

Se han usado de 2 a N capas para implementar el patrón y alejarse de esas estructuras monolíticas. El más conocido es el de tres capas (3-Tier Architecture) desarrollado por John J. Donovan (POSA Volumen 1). Este es un patrón que he aplicado infinidad de veces porque es el que me ha resultado más simple. Autores como Craig Larman (en su libro “Applying UML and Patterns”) y Martin Fowler han propuesto el uso de las tres capas como las siguientes: Presentation layer, Domain Layer (business logic layer or application logic layer) y Data Layer (storage).

Implementar las tres capas ya es un gran paso para desarrollar código limpio. Y es tan simple como usar tres directorios en tu proyecto (independientemente del lenguaje de programación): Presentation, Domain y Data (o App, Domain y Data). Si agregamos la idea de Arquitectura Hexagonal (Hexagonal Architecture), dada a conocer por Alistair Cockburn, podemos considerar a la capa de datos como capa de infraestructura. Es decir que no solo se trata de acceso a datos, sino a cualquier recurso externo (archivos, apis, servidores de email, drivers, etc.). En esta línea, cambiando la nomenclatura, tu proyecto podría contener tres directorios claves: app (Application), domain e infra (Infrastructure).

  • Application: aquí va la interacción con los casos de uso. Todo el código de vista y lógica de presentación (vistas, páginas, estilos, etc.) con el cual interactúa el usuario para acceder a los casos de uso. En esta interpretación incluye la capa UI (si es necesario) y no el core de casos de uso, sino la interface de casos de uso. En mi caso prefiero bajar lo de casos de uso con la lógica de dominio al dominio. En esta capa de aplicación es necesario colocar el “¿Qué hace la aplicación?” o “¿Qué casos de uso tiene?”.
  • Domain: esta capa se encarga del “¿Cómo hacen los casos de uso para cumplir su función?”, es decir: el código de lógica, lógica compartida, de negocio o de dominio. Esto incluye los servicios de dominio y modelo de datos (agregados, entidades, value object, etc.).
  • Infrastructure: aquí va el código de acceso a recursos (acceso a base de datos, recursos, archivos, clientes api para servicios externos, consumo de colas de eventos específicos, etc.).
Ejemplo de 3 capas basado en DDD

Por ejemplo se puede estructurar un proyecto backend de la siguiente manera:

Ejemplo de una estructura de backend MERN NestJS en 3 capas

Patrón de puertos y adaptadores

Claro que con separar en capas no alcanza para tener un código limpio y lejos de ser una madeja de código en monolito. Necesitamos seguir un gran principio en el desarrollo de software: “aumentar cohesión y disminuir acoplamiento”; y eso lo podemos hacer aplicando el principio de inyección de dependencias (“Dependency inversion principle”) con el patrón de “puertos y adaptadores”, también conocida como arquitectura de “Ports and Adapters” o como parte de la Arquitectura Hexagonal.

La idea detrás de la inversión de dependencia es “depender de abstracciones, no depender de implementaciones”. Y para eso vamos a acoplar ‘capas’ por medio de ‘interfaces’. Una capasuperior usara una capa inferior solo por medio de interfaces que funcionan como puertos. Luego, la interfaces será implementadas por adaptadores concretos (implementaciones) de la capa inferior.

En resumen la distinción entre puerto y adaptador es la siguiente:

  • Puerto: Es la interfaz que deberán implementar las distintas variantes de nuestro código para abstraerse de la implementación concreta. En ella se ha de definir la firma de los métodos que existirán.
  • Adaptador: Es la implementación de la interfaz, en ella se generará el código específico para consumir una tecnología en concreto. Esta nunca se usará de forma directa desde otras capas, ya que su uso se realizará a través del tipo del puerto.
Ejemplo de puerto/adaptador en tres capas

De este modo, la capa que usa un puerto usará la implementación inyectada de algún modo, pero solo conoce la interfaz. Hay diferentes formas de hacer inyección de dependencias: inyección por argumento, inyección por constructor, inyección por anotaciones, etc.

Ejemplo con el patrón repositorio.
Ejemplo de inyección de dependencia con anotaciones NestJS (MERN stack) donde se inyecta en el dominio la implementación de infra. Se usa el patrón repositorio (código del ejemplo anterior).

La pauta del dominio como núcleo

Además, si nos guiamos por la propuesta de la Arquitectura Hexagonal que consiste en que el “dominio sea el núcleo de las capas y que este no se acople a nada externo”. El dominio menos acoplado es el modelo de datos compuesto por entidades y objetos de valor. Le sigue el dominio de lógica de negocio ytoda la lógica interna de aplicación. Será nuestro dominio el que contenga los puertos de entrada (incoming) y salida (outgoing). Es decir que toda acción o evento externo (i.e. como pedidos http) que llega a la aplicación por un puerto, es convertido, a través de un adaptador específico, a la tecnología del evento exterior, pasando por el dominio solo a través de sus puertos. Por ejemplo la GUI es un ejemplo de un adaptador que transforma las acciones de un usuario en la API de su puerto. La inversión de dependencia hace que la capa de aplicación y la capa de infraestructura conozcan a la capa de dominio, pero no a la inversa. Y en el caso de la capa de aplicación y la de infraestructura, es la de aplicación quién puede conocer a la de infraestructura y no a la inversa.

Diagrama de dominio como núcleo

Aplicación hexagonal

Una manera de interpretar e implementar esta arquitectura es considerar cada gran pieza de software (que será un proyecto), como una aplicación que empaqueta un conjunto de funcionalidades bajo algún concepto o dominio de problema.

En esta interpretación, la aplicación es el paquete completo, la celda hexagonal. Y esta aplicación es una hexagonal de tres capas, con la capa central como dominio núcleo. Esta aplicación puede ser una standalone desktop, mobile app, web app, una API o un microservicio.

Ejemplo de un API backend hexagonal

Frontend hexagonal

Inclusive, en aplicaciones cliente servidoras u orientadas a micro-servicios, un frontend puede ser desarrollado como una app hexagonal en sí misma, que depende de otras que forman el backend. Habitualmente no se implementa la arquitectura hexagonal de esta manera y se llega a tener frontends que son una verdadera madeja enredada de código. La propuesta aquí es aplicar arquitectura hexagonal también al frontend.

Ejemplo de una aplicación cliente-servidor

Un frontend puede seguir el patrón de tres capas, con la vista UI en la capa aplicación; la lógica del frontend, modelo de datos y dominio en la capa ‘core’ (Domain); y en en la capa de infraestructura el código de llamado a APIs, recursos, o servicios externos.

Ejemplo de estructura de un proyecto frontend en React (MERN stack)

Testing por capas

También tenemos que pensar en el testing. Sin ‘test’ no hay software de calidad. Un beneficio importante de esta arquitectura de software es que facilita la automatización de pruebas independientes por capas usando inyección de dependencias de Mocks y/o Stubs.

Ejemplo de full test por capas

Usar puertos y adaptadores nos ayuda a hacer testing limpio. Cuando se nos complica hacer inyección de dependencia es que ensuciamos el código. Por ejemplo inyectando módulos a un módulo que usa componentes por imports de implementaciones directas. O muchas veces no se hace buen test unitario por la dificultad de aislar el testing para hacer simulaciones.

Estructora de carpetas

Al implementar la arquitectura en un proyecto recomiendo que quede la distinción explícita en 3 directorios (uno por capa). Un ejemplo es como la siguiente:

Estructura de carpetas en un proyecto Back-end

Red de aplicaciones hexagonales

Entonces… ¿Cómo podemos desarrollar ecosistemas de aplicaciones distribuidas con arquitectura limpia? Lo podemos hacer con esta arquitectura hexagonal, distribuida, orientada a microservicios y microapps. Es decir que, en sistemas mayores o en el escalamiento, se pueden conformar sistemas distribuidos como una red de aplicaciones hexagonales interconectadas. Es decir que podemos desarrollar ecosistemas de aplicaciones interdependientes, aunque robustas, y conformadas por micro-apps (incluyendo micro-frontends de ser necesario) y micro-services hexagonales.

Red de equipos ágiles

En honor al principio de Conway, que dice que las estructuras del sistema desarrollado serán congruentes con las estructuras sociales de la organización que lo produce, es que tendremos un ecosistema de equipos acorde a esta arquitectura. En este sentido, tendríamos equipos encargados cada uno de una o varias Apps relacionadas bajo un mismo dominio de contexto. Estos equipos deberían poder desplegar software de forma independiente, en sus propios repositorios y entregar de forma evolutiva e incremental.

Vertical slicing

Y para entregar de forma evolutiva e incremental, los equipos pueden desarrollar desglosando entregables/soluciones en features con ‘vertical slicing’ que a su vez se pueden desglosar en historias de usuario. En un vertical slice pueden haber una o más historias de usuario y/o historias técnicas para completar una feature completa.

Ejemplo de ‘vertical slicing’ de una feature

Desarrollo evolutivo

Estos entregables/soluciones evolucionarían desde algún MVP a productos más complejos, adaptándose a las necesidades de los usuarios según la población foco de cada momento cronológico del ciclo de vida del producto/servicio.

Conclusión

Finalmente, concluyo que estos patrones de arquitectura, como otros semejantes, ofrecen una manera de aplicar buenas practicas de código para crear código funcional, mantenible, de fácil crecimiento y que satisfaga las necesidades de los usuarios. Lo que se recomienda es usar los principios de desarrollo de software que llevan a este tipo de arquitecturas, en busca de encontrar mejores formas de desarrollar software de calidad.

Aquí las claves fueron:

  • Principio de ‘Separation of Concerns (SoC) con patrón de ‘3-Tier Architecture’.
  • Principio de ‘Dependency Inversion’ con el patrón ‘Ports and Adapters’ y el patrón de diseño ‘Dependency Injection’.
  • El principio de ‘Testing’ con el patrón de diseño ‘Dependency Injection’ (de Mocks y Stubs).
  • Y por último, el clásico principio de “más cohesión y menos acoplamiento”.

Espero con este artículo haber podido transmitir esta idea de Hexa3L, que no es otra cosa que una manera de implementar la arquitectura hexagonal.

Referencias:

  1. Single Responsibility Principe (SRP), 2003, Robert C. Martin.
  2. Applying UML and Patterns,1997, Craig Larman.
  3. Presentation Domain Data Layering, 2015, Martin Fowler.
  4. Artículo de softwarecrafters: Arquitectura hexagonal frontend.
  5. Article: Domain Driven Design, Project structure.
  6. Article: What is the 3-Tier Architecture? 2012 by Tony Marston.
  7. Artículo en Medium: Arquitectura Hexagonal.
  8. Article: Hexagonal Architecture applied to typescript react project.
  9. Article: “Hexagonal Architecture: three principles and an implementation example”.
  10. Arquitectura Hexagonal con Typescript en APIs web con Nodejs.
  11. Ejemplo de proyecto backend hexagonal/DDD con NestJs: https://github.com/ecaminero/nestjs-ddd
  12. https://javascript.plainenglish.io/applying-atom-design-methodology-and-hexagonal-architecture-using-react-6dbb1863a5d5
  13. https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c

--

--

Dario Palminio

Enterprise Lean-Agile Coach, líder ágil, ingeniero de sistemas y consultor con experiencia en Transformaciones Digitales.