Proyecto Ecommerce: Arquitectura Hexagonal con Spring Boot - Ejemplo Práctico

Continuamos con nuestra serie sobre patrones de arquitectura de microservicios, y en esta ocasión hablaremos de seguridad. Cada uno de estos servicios se comunicará con el resto (o con un gateway) para completar los flujos de negocio.

Esta es la novena entrega de nuestra serie sobre patrones de arquitectura de microservicios. En capítulos previos, hemos abordado aspectos como la comunicación sincrónica y asíncrona, la orquestación de procesos y la resiliencia ante fallos.

Este artículo parte y toma como base el código y los conceptos mencionados en las entregas anteriores. Ciertamente, creo que el título nos quedó “corto” porque, además de conceptos de arquitectura hexagonal, también hemos visto algunas cosas de Arquitecturas limpias, Domain-driven design y otras. Esta es la tercera parte de una serie, que comenzó llamándose “Arquitectura Hexagonal con Java y Spring”.

El contexto de eCommerce resulta idóneo para ilustrar este patrón, por eso lo utilizaremos como en el resto de posts de la serie, pues suele implicar múltiples dominios y servicios especializados: un catálogo de productos (catalog), un módulo de pedidos (orders), un servicio de pagos (payment), un servicio de logística (shipping), entre otros.

A día de hoy, el software es ubicuo. ¿Queremos chatear con un amigo a miles de kilómetros? Recurrimos al software. ¿Queremos comprar online? Recurrimos al software. ¿Queremos escuchar música? Recurrimos al software.

Lea también: Preguntas Fundamentales para Emprender

Todo software tiene una arquitectura que estará mejor o peor diseñada, pero todo software tiene una serie de elementos que lo conforman y que logran que sea de una determinada manera.

Evidentemente, la fase de diseño arquitectural ha de ser intencional si la resolución de nuestros problemas no es trivial. He comentado antes, literalmente: “la fase de diseño arquitectural ha de ser intencional”. El código espagueti no está diseñado concienzudamente, aunque, a veces, cumple sus propósitos.

Siempre que pienso en el código espagueti se me viene a la cabeza una improvisación musical. Por un lado, en el mundo de la música en particular y del arte en general, la espontaneidad puede ser una aliada ya que facilita la transmisión de tu “yo más interior”, de tu personalidad…

Dicho de otra manera, espontaneidad puede ser confundida con “creatividad en tiempo real”. Esto se percibe socialmente como bueno y de calidad.

Sin embargo, en el mundo del software pasa un poco lo mismo que en el mundo de la arquitectura tradicional (sí, la de los puentes). ¿Pasarías por un puente improvisado que se ha diseñado de forma accidental? ¿Utilizarías una pasarela de pago construida con código espagueti? ¿Te subirías a un coche autónomo que ha programado un desarrollador “en un momento de inspiración”?

Lea también: Cómo iniciar un proyecto emprendedor

Entonces, ¿qué ocurre? ¿Por qué he empezado diciendo que a veces cumple sus propósitos? Porque es así, en determinadas ocasiones no es necesario llevar a cabo sobreingeniería y exprimir al 100% nuestra CPU fisiológica para resolver un problema.

Dicho de otro modo, a veces, nuestro cerebro improvisa implícitamente una solución que funciona y que entra dentro de nuestros objetivos de calidad (funcionalidad, efectividad…).

No obstante, nuestro cerebro no es capaz de improvisar la arquitectura software de los grandes e-commerce, ni tampoco de un coche autónomo. A nivel cognitivo es imposible para un ser humano empezar a escribir el código de un sistema de esas características asegurando, además, que se alcanzan los objetivos de calidad especificados.

Lo que se pretende con esta arquitectura limpia es proponer una metodología de diseño para sistemas complejos que proporciona muchas facilidades para obtener los objetivos de calidad mencionados.

La palabra «separación» implica, en este caso, un bajo acoplamiento (o incluso un desacople total) entre capas, que transitivamente se convierte en una mayor mantenibilidad (responsabilidades acotadas) y una mayor mocks y posibilidad de realizar pruebas unitarias).

Lea también: Emprendimiento en España: Ideas creativas

Además, también favorece la alta cohesión. Para que el diseño sea correcto y no desemboque en una “arquitectura de puertos, adaptadores y espaguetis” es necesario respetar el orden de las capas.

Aunque se suele representar como forma de hexágono (de ahí su nombre), realmente no limita el número de interacciones entre las capas (que sea un hexágono no un cuadrado o un octógono es casualidad). De esta manera, cada lado del polígono sería un puerto al que nos podríamos conectar a través de un adaptador.

Implementamos un adaptador (e.g. Este controlador/adaptador necesitará utilizar un “caso de uso” de la capa de aplicación (e.g. interfaz UserSignIn y clase UserSignInImpl) para registrar un usuario. He definido una interfaz y una clase que la implementa ya que he seguido el Principio de Inversión de Dependencias (la D de los principios SOLID). Este principio se basa en que lo mejor es acoplarse a contratos (UserSignIn) y no a implementaciones concretas (UserSignInImpl).

Este caso de uso de la capa de aplicación necesitará crear una entidad usuario (e.g. clase User) y almacenarla en un repositorio (e.g. interfaz UserRepository). Estos dos últimos elementos mencionados pertenecen a la capa de dominio, el núcleo del sistema en una arquitectura limpia. Recalco que es el núcleo, porque es a lo que se le da más importancia en este tipo de arquitecturas.

Con este pequeño diseño hemos conseguido muchas ventajas. El caso de uso de registrar un usuario se podría utilizar desde distintos lugares que no sean nuestra clase UserController (e.g. El caso de uso de registrar un usuario podría implementarse de diferentes maneras (e.g. El caso de uso de registrar un usuario podría almacenar al usuario en una base de datos documental (MongoDB), en una base de datos relacional (PostgreSQL)… Ya que hemos seguido el principio de inversión de dependencias.

qué es una arquitectura de software, así como qué diferencia al “código espagueti” de arquitecturas bien diseñadas y estructuradas. Dentro de estas últimas, se encuentra la ya famosa “arquitectura hexagonal”, de la que hemos detallado sus ventajas, además de a qué se deben.

En Hiberus somos expertos en microservicios. Trabajamos día a día para mejorar nuestro software basándonos en experiencias previas y en patrones arquitecturales como este.

Vamos al lío, lo primero de todo es cómo llega esta arquitectura a mis oídos. Hace 3 años me encontraba en un proyecto utilizando arquitectura de microservicios, y llegó un buen día un arquitectura y nos explicó la idea de aislar el Dominio, de crear un servicio por caso de uso, nos habló de adaptadores, de puertos… un lío que no veíamos a donde llegaba. Hace unos meses me reencontré con la idea de plantear esta arquitectura a varios proyectos, y decidí investigar un poco sobre el tema.

Realicé un curso, investigué un poco sobre un tema (partiendo de la Wikipedia, por supuesto, y siguiendo por fuentes mas amplias). La arquitectura hexagonal (o patrón de puertos & adaptadores) no es algo nuevo, ni novedoso, ni revolucionario. La idea fundamental de esta arquitectura es aislar la lógica de negocio , o como lo denominan el dominio (¡Ojo! No confundir con Dominio web, o de base de datos) o lógica de negocia del resto de la infraestructura de forma que cualquier cambio en la forma en la que se llama al servicio, repositorios de datos, cambios en el framework, etc no afecto a la lógica de negocio.

Ejemplo Práctico: Cambiar el Nombre de un Cliente

1-. Hasta el momento, tenemos un API REST con un endpoint para obtener los clientes, y otro para crear clientes. Supongamos ahora que nos piden poder cambiar el nombre a un cliente. ¿Que cambios necesitaríamos hacer?

En principio, crear un endpoint con el método PUT, cuya ruta incluya el ID del cliente y que en el body reciba el nuevo nombre para el cliente. Luego deberíamos recuperar el cliente con el ID proporcionado, cambiarle el nombre y volver a persistirlo.

Como vemos, hay varias operaciones “atómicas” o sencillas que utilizamos en esta funcionalidad que seguramente necesitemos reutilizar en muchas otras partes de nuestra lógica de negocio, como el buscar un cliente por ID y el actualizar un cliente.

Encapsular estos casos de uso en objetos nos permitiría reutilizar la lógica donde sea que la necesitemos, y simplificaría el mantenimiento del código, ya que para modificar alguno sólo tendríamos que modificar un objeto. Para esto podemos basarnos en CQRS (Command-Query Responsibility Segregation).

Implementación de Casos de Uso

En primer lugar, vamos a construir nuestro modelo de casos de uso. En segundo lugar, necesitamos una forma única de ejecutar nuestros casos de uso. Haría las veces de un servicio que inyectaríamos allí donde necesitemos ejecutar un caso de uso. Esto podemos lograrlo modelando un UseCaseBus, que será el encargado de saber como se ejecuta cada Command y cada Query.

La pregunta ahora es ¿donde deberían estar estos objetos? Tenemos varias opciones. Podemos, bien extraerlos a un proyecto de arquitectura y agregar en el nuestro la dependencia con este (esta sería la opción ideal, en mi opinión), o colocarlos en un módulo de arquitectura aparte.

Aprovechamos también para crear un wrapper para el contexto, para poder utilizar dentro de nuestra capa domain la inyección de dependencias sin acoplarnos a Spring. Cómo podemos ver, toda la lógica se encuentra dentro de UseCase.

Cómo nuestra lógica de negocio debería ser completamente agnóstica a los detalles de las implementaciones (la base de datos que utilicemos, la forma que decidamos usar para ejecutarla (API REST), y del framework utilizado, etc) , este es un buen motivo para colocar estas clases en un módulo architecture. Esto nos da la posibilidad de poder cambiar cualquiera de estas cosas sin afectar a la lógica de negocio.

Cómo vemos, las clases Query y Command no tienen lógica. De momento, sólo nos van a servir para diferenciar entre Query y Command, pero más adelante podríamos agregar la posibilidad de ejecutar otros casos de uso desde los mismos, y acá es donde cobran valor: Desde un Command, deberíamos poder ejecutar tanto otros commands como queries. Pero desde una Query, no deberíamos poder ejecutar comandos.

Recuperar un Cliente por ID

Primero veamos el caso de uso para recuperar un cliente por su ID. Vemos que el código queda bastante sencillo. Tenemos dos constructores, uno protected, que utilizaremos para poder proveer un mock del puerto en nuestros tests unitarios, y el público, que cómo vemos acepta sólo un ID de cliente y luego utiliza el método locate() para obtener una instancia del puerto secundario en tiempo de ejecución.

Aquí es donde estamos utilizando el wrapper del contexto, al que llamamos ServiceLocator, para poder “inyectar la dependencia” que necesitamos. De esta forma, estamos utilizando de forma desacoplada el contexto de aplicación de Spring.

Cambiar el Nombre del Cliente

Ahora, sólo nos queda implementar el caso de uso para cambiar el nombre del cliente. Para esto orquestaremos las llamadas y la lógica que no tenga un caso de uso propio en otro objeto, como por ejemplo el adaptador primario.

Para esto, uno de los cambios que tenemos que hacer es que el adaptador ya no dependa del puerto secundario, ya que en lugar de interactuar con la capa de persistencia, necesitamos ejecutar casos de uso.

Y cambiar nuestra lógica para llamar a los casos de uso en lugar de ir directamente al adaptador secundario. Los casos de uso son una herramienta muy potente. Permite tener lógicas acotadas y probadas, y reutilizar estas para componer otros casos de uso más complejos, facilitando la comprensión y el mantenimiento del sistema al generar un código mucho más expresivo y fácil de comprender.

tags: #proyecto #ecommerce #con #spring #boot #y