leanmind logo leanmind text logo

Blog

Código Sostenible

Cómo escribir código fácil de mantener mediante valores, principios y técnicas.

Gradle Modules y Domain-Driven Design

Por Borja García

Puedes encontrar código de ejemplo en este repositorio.

Introducción

La Arquitectura Hexagonal, así como otros patrones de diseño similares que implementan Domain-Driven Design, puede ser un enfoque no del todo intuitivo para desarrolladores con poca experiencia (como yo), o con una forma de hacer las cosas más tradicional.

Es fácil confundir conceptos teóricos cuando se pasa a la práctica, y no tener claro en qué capa de la aplicación implementar una clase determinada, o incluso, filtrar información entre capas sin darnos cuenta.

En este post, hablaré sobre cómo Gradle puede servir de ayuda modularizando explícitamente en el código las capas de la arquitectura, siendo el propio compilador quien nos avise si se hace una inyección de dependencias no deseada entre módulos.

Agradecimientos a Carles Espona, compañero de equipo de Holaluz, que fue quien propuso organizar de esta forma un microservicio que desarrollamos, y quien ha hecho de guía en las sesiones de refactoring del mismo, lo que me dio la idea de escribir sobre ello.

¿Qué es un Módulo Java?

Los componentes de uso más común para organizar una aplicación Java, son lo que denominamos paquetes o packages, una estructura de directorios jerárquica que suele ordenarse según estándares que han propuesto los constructores de código más populares, como Maven o Gradle.

Un módulo lleva la abstracción a un nivel por encima de los paquetes. Es básicamente una agrupación de paquetes con un propósito relacionado, acompañados de los recursos que necesitan y un fichero descriptor del módulo que contiene la especificación de sus dependencias. Con todo esto, se construye un JAR independiente para cada uno. Además, en el módulo raíz que contiene al resto, también existe un fichero descriptor que define las dependencias y configuración comunes a varios módulos.

Este enfoque permite una mejor mantenibilidad del código, al facilitarnos el cumplimiento del Principio de Responsabilidad Única en cada módulo o no admitir duplicidad de paquetes ni dependencias cíclicas.

Cabe destacar que, aunque usemos Gradle en este post, desde Java 9 se introdujeron los módulos como una característica propia del lenguaje con el Java Platform Module System. También existe el concepto en otras herramientas como Maven.

Propuesta de división de Capas

Aunque cada desarrolladora tiene su forma de escribir código, y cada proyecto tiene unas necesidades propias, al trabajar con Domain-Driven Design, se suelen encontrar una serie de capas principales y subdivisiones que podrían considerarse un estándar:

La idea es mapear cada capa de la aplicación a un módulo independiente, orquestados por Gradle como veremos a continuación.

Ejemplo de aplicación multi-módulo con Gradle

Para ejemplificar el uso de módulos para implementar capas de Domain-Driven Design, he creado una aplicación Spring Boot sencilla (alojada en el repositorio citado al inicio del post). No es más que una plantilla, con un caso de uso en el que se hace un POST a un endpoint de la aplicación, y se crea el recurso correspondiente en base de datos.

Partiendo de un proyecto Gradle generado con Spring Initializr, el primer paso es reestructurar el árbol de directorios de código fuente. La idea es definir la siguiente estructura:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root
├── src
|   ├── application
|   |  ├── src
|   |  └── build.gradle
|   └── domain
|   |  ├── src
|   |  └── build.gradle
|   └── infrastructure
|      ├── src
|      └── build.gradle
├── build.gradle
└── settings.gradle

build.gradle, es el nombre que recibe en Gradle el fichero descriptor asociado a cada módulo. Como se puede ver en el esquema, con esta estructura se plantean 3 módulos, uno para cada capa principal, además de tener un build.gradle general que describe las dependencias comunes a todos los módulos. El agrupar todos los módulos bajo un mismo directorio src no es obligatorio, es simplemente para mantener limpio el código si en un futuro se desean añadir más módulos independientes al código fuente de la aplicación, como módulos relacionados con pruebas a nivel de infraestructura.

Para que esta estructura modular funcione, lo primero es incluir en settings.gradle cada uno de los módulos a usar:

1
2
3
4
rootProject.name = 'ddd-template'
include 'src:application'
include 'src:domain'
include 'src:infrastructure'

Una vez hecho esto, podemos pasar a configurar las dependencias de cada módulo en su build.gradle, comenzando por el general:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
subprojects {

...
apply plugin: 'io.spring.dependency-management'
...

repositories {
mavenCentral()
jcenter()
}

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testImplementation 'org.assertj:assertj-core:3.17.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

...

}
}

bootJar {
baseName('ddd-template')
}

Lo más destacable de este punto es la cláusula subprojects, toda la configuración declarada dentro de esta, será compartida por todos los módulos de la aplicación. En este caso, estamos estableciendo de qué repositorios públicos se descargarán las librerías, y definiendo las dependencias de JUnit 5 y AssertJ para los tests de los módulos.

Es importante dejar en este fichero el plugin de Spring Dependency Management, que ya incluía el proyecto generado por Initializr, para que la inyección de dependencias funcione correctamente.

El siguiente fichero de configuración será el del módulo infrastructure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
plugins {
id 'org.springframework.boot' version '2.4.1'
}

dependencies {
implementation project(':src:application')
implementation project(':src:domain')
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
...
}

bootJar {
baseName('ddd-template')
}

Es importante destacar que éste es el módulo que contendrá la aplicación de Spring Boot, por lo que las dependencias del framework se deben declarar en este fichero. Esto tiene otras implicaciones, como que cualquier test que necesite levantar el contexto de Spring Boot para funcionar, debe estar incluido en este módulo.

Ahora bien, la clave para mapear las capas de Domain-Driven Design con los módulos, está en la inyección de dependencias, mediante implementation project. Lo que significa esta declaración es que el módulo infrastructure necesita conocer a los módulos application y domain para funcionar.

De la misma forma, el módulo application necesitará conocer el módulo domain, y este último no tendrá ninguna dependencia de los demás.

Como ejemplo de la utilidad de este planteamiento, durante el desarrollo de la plantilla de aplicación me encontré con el siguiente caso:

image-1

Sin darme cuenta intenté pasar un DTO de entrada a la API, ResourceInfo (perteneciente a infrastructure/adapters/input/dtos), como argumento al método resourceCreator.execute().

ResourceCreator es un port declarado en la capa application, la interfaz de un servicio que define el caso de uso. Aunque la interfaz se encuentre implementada como un adapter en la capa infrastructure (en la clase TemplateResourceCreator), el propio caso de uso no lo está, por lo que sin darme cuenta estaba filtrando información de la capa de infraestructura a la capa de aplicación. Como se puede ver en la imagen, el propio IDE me notificó este error con un mensaje “Cannot resolve symbol ‘ResourceInfo’”, sugiriendo como corrección en la declaración del método execute() de la interfaz: “Add dependency on module ‘…infrastructure’”.

Es decir, al no existir una dependencia al módulo infrastructure desde el módulo application, el propio compilador es capaz de avisarme de que estoy incumpliendo la división de capas planteada. La solución a este caso concreto, como puede verse en el repositorio, fue añadir un método en el propio objeto ResourceInfo que devuelva un objeto propio de la capa de aplicación.

Conclusiones

Sin duda, la modularidad es una herramienta interesante, que puede resultar de utilidad como guía para implementar una estructura que siga Domain-Driven Design.

Aun así, hay que tener en cuenta que no es perfecta. Las capas con más dependencias de otras, principalmente la de infraestructura, tienen más probabilidad de ser contaminadas con información ajena, puesto que al tener una dependencia explícita de las otras capas, el compilador nos permite incluir cualquier objeto de las mismas.

Además, aplicar modularidad y/o Domain-Driven Design en aplicaciones simples puede dar lugar a demasiada complejidad innecesaria. La plantilla desarrollada es un buen ejemplo de ello, donde para un caso de uso tan sencillo, fueron necesarios al menos 4 DTOs para transmitir información entre las distintas capas.

Conocer las herramientas es una ayuda, pero no nos exime de cometer errores, el criterio propio y la adaptabilidad a cada contexto siguen siendo necesarios.

Fuentes

Publicado el 25/09/2023 por
Borja image

Borja García

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers