Clean Architecture en la vida real
Crear una aplicación es fácil, sólo hay que sentarse a codificar y … habemus application. Tener una aplicación lista hoy en día es algo sencillo, si estás atascado con algo buscas en internet y encontramos miles de ejemplos y templates que nos ayudarán.
Crear una aplicación es fácil... pero crear una buena aplicación lista para producción es una historia diferente
Para realizar una aplicación, tenemos que estar seguros de los siguientes puntos:
- La aplicación debe ser correcta (solo hace lo que debe hacer y no lo que “no debe hacer”)
- La aplicación debe ser eficiente (se usa solo el tiempo asignado y no consume más recursos de los que debería)
- La aplicación debe ser 24/7.
Este último punto es crítico y adicionalmente necesitamos tener en mente los siguiente:
- Deberá ser fácil de adaptarse al cambio
- Deberá ser fácil realizar mejoras
- Deberá ser fácil de expandirse
Crear una aplicación es fácil y (en principio) muy rápido. La creación de una aplicación excelente, lista para producción requiere un pensamiento cuidadoso, planificación e ingeniería. También puedes optar por la filosofía ‘Déjalo crecer con el tiempo’; La naturaleza tiene millones de ejemplos en los que esto funciona perfectamente bien. La cuestión es, que lleva a la naturaleza millones de años, y es posible que no estés dispuesto a esperar tanto.
Hoy en día uno de los problemas más comunes que se duele dar es la creación de una aplicación basándonos en modelo de datos, dejando de lado el core del negocio.
Esto nos acarrea consecuencias, de las cuales luego se pretende que esa aplicación evolucione fácilmente como nuestro negocio, esto pasa a ser una tarea compleja por que indistintamente sabemos que estamos limitados a la implementación de nuestro core (Bundle Context).
En ocasiones como estás, estamos atados al modelo de datos y sabemos que eliminar esa complejidad nos llevaría mucho esfuerzo y refactor. Otro enfoque que a veces nos aterra, es cuando se construyen aplicaciones del tipo BFF (backend for frontend), dónde nuestros microservicios pasan de ser api-restful a agregadores de modelos.
Con el fin de evitar estos sucesos explicaré los siguientes conceptos de “Clean Architecture”.
Components
Fred Brooks “No Silver Bullet” lo describe de una manera excelente. El desarrollo de aplicaciones tiene problemas al relacionar la “essence” del dominio, y esto deriva en decisiones (el “accidente”).
Para entrar en contexto llamaremos core al conjunto del model entities y user cases.
Este dominio tiene el conocimiento y lo que se quiere implementar a futuro.
El accidente está compuesto por lo siguiente:
- Delivery
- Gateway
- Repository
- External Libraries
- Configuration
- Main
Para que la “essence” funcione en un entorno real necesitaremos de frameworks y los componentes citados anteriormente lo llamaremos Infrastructure
Modelo de Componentes
Core
El core se describirá como dominio , como el model entities y los use cases. Estos términos están relacionados con el core, en otras palabras, aquí está representado el dominio y las “features (ports)” que queremos implementar.
Model Entities
Son nuestros objetos de negocio que representarán nuestro dominio en interacción con los use cases.
Use cases
Los casos de uso tienen reglas de negocio específicas de la aplicación
Encapsula e implementa todos los casos de uso de la aplicación. Estos use cases organizan los flujos de datos hacia y desde las entidades. Además controla que esas entidades usen sus reglas de negocio para toda la aplicación, y lograr los objetivos del mismo.
Infrastructure
Será quien nos dará el soporte para que nuestra aplicación funcione.
Delivery
En delivery, encontramos los adaptadores de interfaz, que reciben solicitudes desde el exterior del microservicio (de ahí su nombre :-)).
Es común implementarlo como un servicio http rest, o consumir mensajes de algún intermediario de mensajes (como cualquier servidor JMS, RabbitMQ, etc.).
Gateways
Los gateways son adaptadores de interfaz que permiten que este microservicio solicite servicios a otros microservicios (legacy or external services).
Es común ver implementaciones como las llamadas http, clientes de message broker o cualquier otra API.
Repositories
Estos adaptadores son destinados a almacenar y recuperar objetos de una aplicación (serializados), mayormente estos representan una entidad en nuestro modelo de persistencia.
La diferencia entre los respositories y los gateway es que este último es usualmente utilizados para comunicarse con otros sistemas en cambio el repository se comunica con todo lo que corresponda internamente al microservicio. Esto pertenece al limite lógico del Bounded context ( Domain driven design).
Configuration
La configuración es la parte de la aplicación que define los comportamientos los diferentes componentes para que una aplicación se ejecute.
Contiene las factories de los componentes y realiza la inyección de dependencia para vincular los diferentes componentes, de tal manera que en este módulo creamos nuestra aplicación según las implementaciones que vayamos realizando.
También tiene la lógica para recopilar datos de configuración, parámetros de aplicación, variables de entorno o archivos de configuración con el objetivo de que la aplicación sea parametrizable .
Main
Su propósito es llamar a la configuración y darle run a la aplicación.
Alguien lo tenía que hacer.
Process flow
Para que las palabras anteriores tengan sentido es necesario que los componentes interactúen entre ellos de la siguiente manera:
Request Process Flow
- Un sistema externo realiza una solicitud (HTTP, un mensaje JMS, un JOB, etc.).
- El delivery crea varias model entities a partir de los datos de la solicitud.
- El delivery llama a una acción del user cases (comando, interacción, etc.)
- El use case opera en model entities.
- El use case hace una solicitud para leer / escribir en el repository.
- El repository consume alguna model entity de la solicitud de caso de uso.
- El repository interactúa con la persistencia externa (DB SQL, Command line, system commands, etc).
- El repository crea los model entities a partir del modelo de datos de nuestra persistencia.
- El use case solicita colaboración de un gateway.
- El gateway consume las model entities proporcionadas por la solicitud del use case.
- La gateway interactúa con servicios externos (otras aplicaciones, coloca mensajes en colas, imprime en una impresora, etc.).
Dependencies
Este diagrama marca un poco la “dependencia entre las capas”
En la imagen, las secciones más oscuras están relacionadas con procesos detallados y específicos de la tecnología (accident), mientras que las más claras son más conceptuales (essence).
En detalle:
- Todo depende del model entities que representan nuestro negocio, estos deberían ser los elementos más estables y los más importantes.
- El delivery, los repositories y los gateways (la capa de infraestructura en clean architecture y otros modelos) dependen del use case (las reglas de negocio de la aplicación) y las model entities. Estos dos últimos son el core del negocio.
Todos los componentes pueden depender de frameworks compatibles:
- Las model entities requieren de frameworks para tener listas, conjuntos, etc. Además, podrían necesitar alguna librería para soporte de fecha y hora, etc.
- Los uses cases necesitan soporte para el procesamiento de colecciones, la lógica de negocio, la gestión de concurrencia (sí, no siempre se puede configurar de forma transparente) y otros.
- El delivery necesita soporte del servidor HTTP, JSON u otras conversiones, validaciones JWT, etc.
- Los gateways requieren clientes HTTP, clientes del servidor de cola, etc.
- Los repository requieren la conexión y el cliente a la base de datos.
Java Implementation
Model entities
Las model entities son simple clases de Java (POJO tiene más estilo y apariencia de marketing).
Use Case
Los use case se implementan mediante un conjunto de acciones en donde toda la lógica de negocio es implementada, aquí se realizará el tratamiento de nuestros objetos de dominio así como también las llamadas a nuestros repository y gateways.
Ejemplo de un use case:
Este use case es simple, sólo importa el repository para traer todas las categorías persistentes, pero de ser necesario se pueden implementar otros gateway o repositories, todo lo que sea necesario para complementar el modelo o ejecutar mi acción.
Delivery
El delivery consta de un grupo de controladores (handlers). Cada controlador maneja solicitudes específicas, convierte el payload a DTO o model entities, y llama a la acción apropiada. Finalmente, mapea la respuesta del use case y retorna la respuesta correspondiente a quién lo invoque.
El controlador no tiene lógica más allá de la transformación (serialización, deserialización) y la llamada a la acción. Toda la lógica de negocio debería estar detrás del use case.
Gateways
Aquí el Gateway aprovecha de FeignClient (frameworks) provisto para conectarse a un servicio externo.
Una vez más, el Gateway no tiene lógica de negocios, sino conversiones y mapeos.
Repositories
En la persistencia se implementan los repository propios de JPA o los customs repository, además de inyectar los converter.
Configuration
Aquí configuraremos el comportamiento del persistence, deliveries y sobre todo nuestros use case. Si queremos que algunos de los componentes tengan un comportamiento diferente es tan simple como implementar otro comportamiento del bean.
Main
La clase más aburrida:
Pero, ¿Cómo quedaría nuestra package structure?
Core
En este ejemplo se muestra cómo está implementado el core en un multimodule de maven.
Esta estructura es un modelo del core dónde separamos las excepciones propias de los use case que no están sujetas a ningún framework
Podemos tener separado los namespace por dominios, entidad y el recurso que sea necesario. También se tiene un namespace con los ports dónde se alojarán las interfaces para comunicarse con el exterior (infrastructure)
Infrastructure
En cada namespace (package) tiene una responsabilidad diferente, se verá que en configuration cuando se define comportamiento de la aplicación como use cases.
Entre cada es posible tener las subdivisiones diferentes que ayuden a mantener organizado el proyecto.
Existe un namespace denominado shared dónde se alojarán las clases compartidas por toda la infraestructura.
Repositorio de ejemplo
Aquí encontrarán el código fuente con la estructura de carpeta de un multimodule maven.
Conclusiones
Crear grandes aplicaciones es un desafío, pero una buena estructura y el seguimiento correcto de algunos principios pueden ayudar mucho en su desarrollo. La arquitectura que vimos en estos pasos nos permite crear aplicaciones que son fáciles de entender, fáciles de cambiar y fáciles de solucionar. La separación correcta de las capas nos proporciona una base sólida.
Lo más importante es que este árticulo sirva como guía. Comenzar a desarrollar con esta arquitectura puede que conlleve un esfuerzo adicional, pero luego la evolución del mismo es más fácil de aplicar, así como de entender.
Como dicen clean architecture es una serie de reglas y principios:
- SOLID
- DRY
- SRP
- KISS
- Clean Code
Si lo aplicamos de una manera coherente el proyecto será clean.
Referencias: