Clean Architecture + Springboot + JPA (Parte 1)
Repositorio
Motivación
Esta es la primer parte, la segunda es: Parte 2
https://jesusledesma.medium.com/clean-architecture-springboot-jpa-parte-2-e6556f528d3c
Clean Architecture está basado en el libro “Clean Code” de Robert “Uncle” Martin. Es un ingeniero muy influyente con muchos seguidores, que siguen sus consejos de buenas prácticas de desarrollo
En mi experiencia, cuando buscaba información de este tema, noté que muchas personas aplican Clean Architecture de formas diferentes. Su forma de ordenar los paquetes, módulos y clases son diferentes, varían dependiendo del lenguaje e inclusive del framework
Debido a esta situación, me propuse aportar a la comunidad mi versión de la aplicación de Clean Architecture. Busco aplicar los conceptos de la misma, pero de una forma que sea “más amigable” para quien viene de la estructura que generalmente se usa (Controller, Service, Client, Repository). Y además que el ejemplo sea una aplicación con un framework como Springboot + JPA, ya que es el más usado actualmente por aquellos que utilizan java o kotlin en backend
Características de una arquitectura limpia
Independiente de los frameworks
Los frameworks deberían ser herramientas, y no obligarnos a actuar de una determinada manera debido a sus restricciones, por el cual deberiamos poder cambiar el framework que usa nuestra app todas las veces que nos apetezca y que esto nunca afecte al core de la misma.
Testable
Debemos poder probar nuestras reglas de negocio sin pensar en base de datos, interfaz gráfica u otros componentes no esenciales de nuestro sistema.
Independiente de la UI
Si la UI cambia a menudo esto no puede afectar al resto de nuestro sistema, que tiene que ser independiente.
Independiente de la base de datos
Deberíamos poder cambiar de Oracle, a SQL Server, a MongoDB, a Casandra o a cualquier otra base de datos sin que afectara demasiado a nuestro sistema.
Todas estas características, según Bob Martin, se agrupan en el siguiente gráfico:
Observaciones
Invito al lector a profundizar en la teoría para tener un mejor panorama de los conceptos.
Como mencione antes, quiero aplicar Clean Architecture, pero utilizando las ideas de cómo se estructuran los proyectos MVC. Por lo tanto dejo un pequeño “mapeo rápido” de conceptos
Domain
¿Cómo pensamos una app?
Cuando comenzamos desde cero a crear una aplicación hay varias formas de comenzar, y depende del contexto. Hay personas que comienzan por los controllers, ya que tienen definidos los datos que recibirán, hay otros que comienzan por el dominio de objetos, otros por el modelo de datos y otros con una combinación de las anteriores.
En nuestro caso comenzaremos por el dominio de objetos pensando en que guardaremos en la base ya que usaremos JPA con hibernate como ORM
Nuestra App
Nuestra aplicación está basada en una app “Real” que genera contratos para personas que quieren pedir un crédito. Necesitamos conocer los datos de nuestro cliente, y en base al tipo de producto y brand de producto, le generamos un contrato.
Un contrato está compuesto por 2 templates, uno llamado Mutuo y otro llamado Pagaré
En la aplicación productiva, efectivamente se genera un contrato pdf, nosotros a fin de simplificar el código, ante la petición de generación de contrato, retornaremos un id de contrato y una lista con los nombres de template de contrato
Objetos de Dominio
Lo primero que podemos notar en la estructura de paquetes la “capa 1 (Dominio)” y la “capa 2 (Servicios)” estará agrupada en el paquete llamado core. A su vez, dentro de este paquete, organizaremos todo en base a entidades de nuestra aplicación, en nuestro caso manejamos contratos, por el cual el primer paquete será Contract.
Dentro de Contract habrá dos paquetes, Domain y también Service, así agruparemos la capa 1 y la capa 2
Un contrato está compuesto por un id, un userId y un Producto
A su vez un producto por un type, un brand y una lista de Templates
Y un Template tiene un nombre
Por el momento todos estos objetos de dominio estarán en el paquete Domain del paquete Contract
Hasta el momento hemos diseñado nuestro modelo de objetos, básico, que se traducirá a información que guardaremos en la base de datos. Sabemos que necesitamos guardar un contrato con su producto, y a su vez este con su template
Hemos completado la “capa 1 (Dominio)”, es bastante simple, pero sobre ella construiremos una aplicación usando Clean Architecture
Otra cosa a aclarar en que se pueden ver annotations como @Data y @AllArgsConstructor. Es una librería llamada Lombok, escribe los setter, getters y constructores por nosotros para tener menos código y que sea más rápido de leer
Contract Controller
Requerimientos y reglas
Nuestra aplicación, está conectada al mundo. Tiene una entrada, un procesamiento, y una salida de datos. Por reglas de negocio, será utilizada por otra app para crear un contrato.
Esta app posteara a un endpoint que llamaremos /contract. Los datos que nos enviaran son id de producto y id de usuario
Sabemos que debemos generar un contrato, guardarlo en la base de datos y a nuestro cliente retonarle la información id, una lista de templatesNames
Lo primero que debemos saber es que crearemos un controller, pero que no lo acoplaremos a un framework (Al menos en esta capa). Simplemente nos manejaremos con objetos, con entrada y salida de datos
Como es un post y nos envian varios datos (los antes mencionados) el método generateContract recibirá un objeto ContractInput y como salida otro objeto ContractOutput
Input y Output
Contract Input
Está compuesto por los datos que mencionamos en los requerimientos
Y se guardaran dentro del package domain/input, con la siguiente estructura
Contract Output
Está compuesto por los datos que mencionamos en los requerimientos
Y se guardaran dentro del package domain/output, con la siguiente estructura
La idea es que todo objeto que ingrese a nuestro sistema y que esté relacionado con un contrato esté en el package input. A su vez, todo lo que salga de nuestro sistema, a uno externo esté dentro del package output
Input y output son parte de nuestra “capa 1 (Dominio)” y la clase ContractController es parte de la “capa 3 (Interfaces Adapter)”
Product Service
Recordemos que un contrato tiene un producto, por el cual antes de pensar en un contrato debemos obtener un producto. El único dato que nos dan es el productId que recibimos del controller en el objeto ContractInput, entonces sabemos que deberíamos tener algo como lo siguiente
Note que ahora ContractController necesita de un ProductService, para en base al productId obtener un Producto para generar un Contract
En ningún momento en el controller se agregó anotaciones de inyección de dependencias o de manejo web, y es parte de la idea de Clean Architecture, que nuestro código quede desacoplado del framework
Reestructuración de paquetes
Bien, ese es el primer vistazo de cómo usaremos este servicio, pero antes debemos hacer una reestructuración de paquetes. La clase Producto estaba dentro del paquete domain, que a su vez estaba dentro del paquete contract (contract/domain/Product.class). Como ahora tenemos un servicio para manejarla idea de productos, deberemos asignarle un paquete propio a esta idea de nuestro modelo de negocios.
¿Cuando saber que es momento de separar ideas de nuestro modelo? Cuando son tan fuertes que necesitan un Service o un Controller para manejarlas, de lo contrario pueden pertenecer al mismo paquete que su objeto contenedor, como es el caso de Template
Ahora el paquete core está constituido por dos conceptos de negocio contract y product
A su vez se agregara un nuevo paquete dentro de Product, al mismo nivel del paquete domain. Lo llamaremos Service, y dentro tendrá una clase ProductService
Al agregar el paquete service estamos entrando a la zona de la “capa 2 (Servicios)”
ProductClient
A nosotros nos llega desde el controller el productId, con el cual le consultaremos a un servicio externo. Este servicio externo nos retornara el type y el brand de este producto. En base a ese type y brand (Que son claves primarias en nuestro modelo de datos) podremos saber qué templates tendrá el producto, y por ende, nuestro contrato.
Tal vez te preguntes ¿Por que mejor no guardar todos los ids de productos en nuestra base? Simple, no somos la app encargada de eso, gracias a los microservicios, otra aplicación se encarga de manejar todo lo relacionado a productos, nosotros lo único que necesitamos es saber qué templates usará en base a un type y un brand, y nosotros tenemos la responsabilidad de tener la info de los templates
Debido a que tenemos que comunicarnos con un servicio externo, necesitamos crear un Client. Los clients forman parte de la “capa 3 (Interfaces Adapter)”
En principio creamos una interfaz ProductClient esta interfaz será usada por nuestros services (capa 2). De esta forma nuestra capa 2 y capa 1 no sabe qué tipo de implementación particular usarán nuestros client lo cual no da la posibilidad de usar la que mejor se adapte a nuestra necesidades
En principio nuestro ProductClientImlp tendrá dos atributos, uno es la url a la que consultara y otro es un restTemplate, que es un objeto que provee spring para realizar pegadas REST. A su vez implementara ProductClient, ya que por medio de este nos comunicaremos con los services, en clean-architecture nunca lo haremos directamente desde la implementacion. Recordemos que podemos tener muchos tipos de implementaciones, si quisieramos podriamos no usar RestTemplate y usar un objeto de una libreria para consumir APIs
ProductClientImlp tiene un método que será usado por ProductService llamado getProductIn, que retorna un objeto llamado ProductIn del cual hablaremos más adelante, pero que contiene la información ya mencionada
Note como son atributos y no usamos @value para la url, como generalmente se suele usar en los clients, ya que @value nos permite traer info del archivo properties (Donde se guarda info de configuración como los host de los servicios externos)
También sucede con RestTemplate, no usamos @autowire para inyectarlo como generalmente hacemos en los clients. No lo hacemos porque nos queremos desacoplar del framework en esta capa, todo esto se realizará más adelante en la capa de framework
En la siguiente imagen vemos la implementación del método que será usado por ProductService y cómo hacemos uso de restTemplate y de la url
Algo importante a destacar es cómo se repite la idea de que cuando algo sale de nuestro sistema agregamos el objeto al paquete output y cuando entra, al paquete input. Solo que ahora sucede con Product
ProductSqlRepository
Ya contamos con la información que fuimos a buscar a nuestro servicio externo. Tenemos un type y un brand. Ahora con ello podremos ir a nuestra base de datos para recuperar un Product que usaremos en ContractController
ProductService terminado luce así
Como se puede ver, Creamos en el paquete repository una nueva interfaz ProductSqlRepository y le declaramos el método findProduct que hace uso de ProductIn
Nótese que en el nombre dice “Sql” eso es porque más adelante tendremos otros repos que serán documentales
JPA y Capa 4 (F y D)
Pero ahora debemos ir más allá, una de las ventajas de usar un framework es que nos simplifica muchas cosas, como por ejemplo el acceso a datos. En Spring se realiza por medio de JPA
Nuestro “capa 2 (Servicios)” jamás interactuara con una interfaz de JPA ya que pertenecen a la “capa 4 (F y D)”, ni con objetos de su modelo, por eso es necesario crear una interfaz “Libre de framework” para que sea usada por los servicios. Por eso creamos la interfaz ProductSqlRepository que funcionará como puente entre los dos mundos
¿Pero cómo logra ProductSqlRepository conectar el mundo de JPA que es parte de la “capa 4 (Framework y Drivers)” con nuestra “capa 2 (Servicios)” que es “Libre de framework”?
Bueno, a ello vamos en esta sección
Modelo JPA
Si ya has utilizado JPA sabemos que hay que crear un modelo de objetos que se asocia a una tabla de la base de datos por medio de annotations, este tutorial no está enfocado en JPA por el cual da por sabido este tema
Creamos nuestro primer objeto de nuestro modelo de JPA, lo llamaremos ProductSql. Pero lo importante a entender es que el objeto ProductSql no es conceptualmente igual a Product de nuestro dominio, ya que este último está libre de annotations, por el cual está desacoplado del framework
Se agregó un paquete llamado domain en donde se guardará el dominio con las anotaciones de JPA. En un proyecto que no use clean-architecture también se hace una diferenciación entre los objetos de jpa y los usados en el dominio, ya que en los objetos jpa hay redundancia como se puede ver en los @ManyToMany, que causa que no se puedan serializar ni deserealizar correctamente a json
A su vez se crea el objeto jpa para el objeto template
JPA Repository DAO
El paso siguiente es crear nuestro DAO repository que extendera de CrudRepository, clase de JPA que nos permite ahorrarnos un montón de código
Es una clase genérica que necesita que ser generada por la clase JPA que queremos retornar, y por el identificador de la misma
Declaramos el método que queremos usar, en este caso queremos buscar por type y brand. Lo genial de JPA es que no necesitamos definir el cuerpo del método
Product Sql Adapter
Pero ahora ¿Cómo conectamos nuestro DAO con nuestros Servicios? Ya que los Servicios usan la interfaz de ProductSqlRepository, que retorna un objeto del tipo Product y no ProductSql
Lo haremos por medio de un Adapter, este es el punto clave de nuestro sistema para mantener desacoplado nuestra “capa 2 (Servicios)” de la “capa 4 (F y D)”. Un adapter nos permite trabajar con objetos de nuestro dominio como Product además de que los Servicios no tendrán una dependencia directa a un DAO que posee código particular del framework
Así luce el adapter terminado
ProductAdapter debe implementar la interfaz de ProductSqlRepository y tener como dependencia a ProductSqlDao. La primera para respetar el mismo comportamiento que expone la interfaz, y el segundo para recuperar de la base el objeto JPA
La idea es transformar el objeto ProductSql a un objeto de nuestro dominio Product, en mi caso lo hice facilmente gracias a ModelMapper, es una librería que permite hacer rápidamente estos mappeos de objetos pero tambien se podria hacer de forma manual
Conceptos de Repository (Importante)
JPA nos permite cambiar de base de datos SQL de forma muy simple. En nuestro caso usamos MySql, si quisiéramos usar otra, es tan simple como actualizar el pom y cambiar la configuración en las properties.
Pero qué pasaría si quisiéramos usar una base de datos SQL no soportada por JPA? Deberíamos cambiar algunas cosas de la “capa 4 (F y D)”, pero nuestra “capa 1 (Dominio)”, nuestra “capa 2 (Servicios)” y nuestra “capa 3 (Adapters)” seguirán exactamente igual.
Supongamos que tenemos que hacer “a mano” el proceso de CRUD en la base, usando el driver de la misma. Este código estaría en el paquete DAO, y de ser necesario también en Adapter y Domain (del paquete repository), pero como nos manejamos con una interfaz para comunicarnos con la capas 3 y 2, ninguna de estas se verá afectada. El cambio es limpio y transparente
Exceptions
Por último se puede ver que en el caso que no exista el producto buscado lanzamos una excepción
Las excepciones son objetos core de nuestro sistema, por el cual van en este package, dentro de un nuevo paquete llamado Exception
ProductService Terminado
Como mencionamos antes así luce ProductService terminado
El paso siguiente es usarlo en ContractController, para que con el Product que retornamos, podamos crear un Contract
Parte 2
En la parte dos terminaremos:
- ContractController, creando un ContractService en cual creara y guardara contratos en una base de datos NoSql (Mongo)
- Realizaremos la inyeccion de dependencias sin acoplarnos al framework
- Realizaremos toda la parte web sin acoplarnos al framework