¿Cómo podemos usar arquitectura limpia para desarrollar ecosistemas de aplicaciones distribuidas con equipos ágiles?
Una forma es usando arquitetura Hexa3 que a continuación paso a comentar.
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.).
Por ejemplo se puede estructurar un proyecto backend de la siguiente manera:
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.
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.
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”. Entonces 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.
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.
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.
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.
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.
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:
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.
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:
- Single Responsibility Principe (SRP), 2003, Robert C. Martin.
- Applying UML and Patterns,1997, Craig Larman.
- Presentation Domain Data Layering, 2015, Martin Fowler.
- Artículo de softwarecrafters: Arquitectura hexagonal frontend.
- Article: Domain Driven Design, Project structure.
- Article: What is the 3-Tier Architecture? 2012 by Tony Marston.
- Artículo en Medium: Arquitectura Hexagonal.
- Article: Hexagonal Architecture applied to typescript react project.
- Article: “Hexagonal Architecture: three principles and an implementation example”.
- Arquitectura Hexagonal con Typescript en APIs web con Nodejs.
- Ejemplo de proyecto backend hexagonal/DDD con NestJs: https://github.com/ecaminero/nestjs-ddd
- https://javascript.plainenglish.io/applying-atom-design-methodology-and-hexagonal-architecture-using-react-6dbb1863a5d5
- https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c