En Mi Local Funciona

Technical thoughts, stories and ideas

Validando una Arquitectura con ArchUnit (Parte 3)

Publicado por Víctor Madrid el

Arquitectura de SolucionesArchUnit

En este tercer artículo de la serie "Validando una Arquitectura con ArchUnit" se va a enseñar a utilizar ArchUnit con un ejemplo real y desde un enfoque que podría ser el de cualquier área de Arquitectura de una compañía.

¿Pinta interesante?

A modo de recordatorio pongo los enlaces a los artículos anteriores :

Para poder abordar el ejemplo real del que hablamos y que no quede como un ejemplo típico de "Hello World", implementaremos dos librerías de arquitectura específicas donde cada una tendrá su propio enfoque, que proporcionarán las Reglas Arquitectónicas y el soporte para su uso en testing, luego implementaremos dos aplicaciones diferentes interrelacionadas cada una también con su enfoque para finalmente validar las Reglas Arquitectónicas sobre esas dos aplicaciones.

Por cierto, una de las aplicaciones será un "microservicio" :-)

¿No habíamos venido a "jugar" con algo real?

Pues jugaremos de forma seria :-)

Este artículo está dividido en 5 partes:

  • 1. Premisas de la Arquitectura que se quiere validar
    • 1.1. Premisas Generales Globales
    • 1.2. Premisas Generales de la Plataforma de Desarrollo
    • 1.3. Premisas Generales de la Herramienta de Construcción Automática
    • 1.4. Premisas Generales del Stack Tecnológico
    • 1.5. Premisas Generales de la Nomenclatura
  • 2. Diseño de las Reglas Arquitectónicas
    • 2.1. Aspectos a cubrir con las Reglas Arquitectónicas
    • 2.2. Premisas del diseño de las Reglas Arquitectónicas
      • 2.2.1. Premisas para la Capa API "Core"
      • 2.2.2. Premisas para la Capa API "Lang"
      • 2.2.3. Premisas para la Capa API "Library"
  • 3. Diseño de librerías de Arquitectura para el soporte de ArchUnit
    • 3.1. Librería "acme-architecture-testing"
    • 3.2. Librería "acme-architecture-spring-testing"
  • 4. Ejemplos de Uso
    • 4.1. Aplicación "acme-api-greeting-model"
    • 4.2. Aplicación "acme-api-greeting"
  • 5. Conclusiones

1. Premisas de la Arquitectura que se quiere validar

Debido a que utilizaremos premisas o consideraciones lo más parecidas posibles a las que nos podemos encontrar en el mundo laboral "real", se ha decidido aclarar previamente cuales han sido los premisas "principales" que se han elegido, con vistas a ayudar a entender este artículo y sobre todo los ejemplos utilizados.

Los premisas "principales" han sido agrupados en categorías según su ámbito.

Categorías de Premisas :

  • 1.1. Premisas Generales Globales
  • 1.2. Premisas Generales de la Plataforma de Desarrollo
  • 1.3. Premisas Generales de la Herramienta de Construcción Automática
  • 1.4. Premisas Generales del Stack Tecnológico
  • 1.5. Premisas Generales de la Nomenclatura

1.1. Premisas Generales Globales

drawing Photo by William Iven on Unsplash

Partimos de que somos un área de arquitectura de una compañía cualquiera (por ejemplo : ACME) y queremos validar la arquitectura de nuestros proyectos de forma que el propio mecanismo de validación de la arquitectura sea :

  • Estándar : Que sirva como tipo, modelo, norma, patrón o referencia
  • Normalizado : Poniendo en orden o regularizando algo que no lo estaba -> "Estabilización en lo que se considere normal"
  • Centralizado : Que varias cosas dependan de un elemento central común -> Reutilización
  • Homogéneo : Que todo sea de un mismo tipo y posea de unas mismas características

¿Os suenan estas cosas?

La propuesta que haremos para solucionar todos estos requerimientos será la creación / implementación de unas librerías de arquitectura, estas librerías serán cargadas como dependencias desde los proyectos que utilicemos según necesidades y proporcionarán todo lo necesario para validar arquitecturas (reglas + soporte para testing).

1.2. Premisas Generales de la Plataforma de Desarrollo

En este caso, la plataforma de desarrollo elegida será Java, como esta tiene muchas implementaciones y muchas versiones en concreto he seleccionado OpenJDK 8.

Como área de arquitectura de una compañía deberíamos siempre pensar en tener todos los desarrollos lo más homogéneos que sean posibles. Por lo que si vamos a incluir una plataforma nueva a la que dar soporte habría que valorar si compensa añadir esa plataforma antes de elegirla con pros y contras, así como controlar que aplicaciones estan con cada plataforma lo cual se podría hacerse con un mapa de sistemas e incluso planificar una migración de aplicaciones entre plataformas.

Nota

Lógicamente he elegido Java porque Archunit es una librería Open Source para testing de Java.

Quiero dejar claro que podríamos haber elegido cualquier plataforma Java y casi cualquier versión.

Aspectos a tener en cuenta :

  • La elección de la plataforma y la versión puede condicionar la definición de cierto tipo de reglas de arquitectura cuando se trata de validar alguna funcionalidad específica proporcionada por ella
    • Por ejemplo : alguna anotación, alguna clase específica, etc.

1.3. Premisas Generales de la Herramienta de Construcción Automática

Se ha elegido la herramienta de construcción automática Maven 3 que esta pensada para su uso en proyectos Java y que es muy popular en el mercado por sus características.

Características :

  • Facilita un fichero de control del proyecto donde se definen las características, propiedades y estructura del proyecto
  • Soporta diferentes tipologías de artefactos : JAR, WAR , EAR, ....
  • Permite la homogeneización y estandarización de los desarrollos : comandos comunes, parent POM, etc
  • Permite la construcción automática de proyectos
  • Facilita la integración de testing dentro del ciclo de construcción
  • Proporciona mecanismos para la creación y gestión de arquetipos
  • Proporciona mecanismos para la gestión de dependencias : definición, paquetería, versionado y su resolución (directa o indirecta)
  • Proporciona un repositorio de dependencias local
  • Dispone de un amplio catálogo de recursos públicos (repositorios de "librerías") de acceso a dependencias → Permite añadir otros repositorios
  • Facilita la generación de módulos
  • Posibilidad de ampliación de funcionalidades con plugins : propios o de terceros
  • Permite generación del site
  • ...

Como área de arquitectura de una compañía deberíamos tratar de normalizar lo máximo posible el proceso de construcción automático, el testing unitario y sobre todo la forma en la que se harán las cosas (configuración de plugins, existencia de test de un tipo determinado, etc.)

Aspectos a tener en cuenta :

  • Uso del arquetipo estándar para el desarrollo con Java con propósito general de los proyectos
    • Ubicación de código, recursos y test en rutas por defecto
    • Etc.
  • La gestión de dependencias será "básica" tratando de utilizar las últimas versiones existentes en el mercado y tratando de garantizar la compatibilidad de todo
    • Normalización entre aplicaciones de forma general
    • Normalización entre tipologías de aplicaciones
    • Las particularidades dependerán de cada aplicación
  • La gestión de plugins será "básica" tratando de utilizar las últimas versiones existentes en el mercado y tratando de garantizar la compatibilidad de todo
    • Normalización entre aplicaciones de forma general
    • Normalización entre tipologías de aplicaciones
    • Las particularidades dependerán de cada aplicación

En mi caso particular muchas veces añado ciertos plugins extras que proporcionan las propias herramientas de construcción y que nos puede ayudar a dar soporte en temas de arquitectura como, por ejemplo:

  • Apache Maven Verifier : Comprueba la existencia o no de ficheros / directorios e incluso aspectos de su contenido
    • Por ejemplo : comprobar si existe el fichero Dockerfile, comprobar si existe un fichero de propiedades para un entorno concreto
  • ...

1.4. Premisas Generales del Stack Tecnológico

Para el desarrollo y el testing se ha elegido un Stack Tecnológico muy interesante, típico en el mercado actual y tratando de utilizar sus últimas versiones.

Stack Tecnológico:

General

  • Java 8 con OpenJDK8
  • Maven 3 como herramienta de construcción automática y gestor de dependencias

Para el desarrollo

  • El framework de Spring - Spring Boot
  • Uso de starters de Spring necesarios : spring-boot-starter-test, spring-boot-starter-web, spring-boot-starter-data-jpa, etc.
  • Uso de Lombok para la generación automática de getters, setters, equals, hashCode , toString y más
  • Uso de Mapstruct para el mapeo de clases
  • Uso de Jackson para el tratamiento de clases Java como objetos JSON y viceversa
  • Uso de springfox-swagger para la documentación de APIs REST

Para el testing

  • Uso de JUnit 5 para el testing unitario / integración
  • Uso de Mockito para usar mocking en los tests
  • Uso de Hamcrest para matchers
  • Uso de H2 como motor de persistencia in-memory para su uso en testing y para la emulación del entorno "local"
  • Uso de Liquibase para el control de cambios en base de datos y en este caso para ayudar a montar una H2 operativa en el entorno local
  • Uso de Archunit para el testing de arquitectura

Como área de arquitectura de una compañía deberíamos de pensar en homogenizar nuestros desarrollos lo que se pueda, apostar por una serie de tecnologías, apostar por una forma de hacer las cosas y sobre todo pensar que una arquitectura evolucionada con el tiempo.

Aspectos a tener en cuenta :

  • Recordar la transitividad en el uso de dependencias
  • Recordar que la elección de ciertas tecnologías imponen ciertas implementaciones así como versiones concretas

1.5. Premisas Generales de la Nomenclatura

Para el establecimiento del naming adecuado de cada una de las partes que se desarrollarán se han seguido diferentes convenciones:

  • Convenciones de nomenclatura de proyecto
  • Convenciones de nomenclatura de paquetes
  • Convenciones de nomenclatura de clases

Convenciones de nomenclatura de proyecto

Para este tipo se ha seguido el siguiente formato :

{COMPAÑIA}[-{PROYECTO / APLICACION}]?[-{TIPOLOGIA}]?-{DESCRIPCION}

Detalle :

  • {COMPAÑIA} : texto que representa el nombre de la compañía propietaria
  • {PROYECTO / APLICACION} (opcional) : texto que representa el nombre de la aplicación / módulo / librería
  • {TIPOLOGIA} (opcional) : texto que indica el ámbito o tipo de la aplicación / módulo / librería
  • {DESCRIPCION} : texto que dar información descriptiva sobre la funcionalidad de la aplicación / módulo / librería

De esta forma se podrá identificar de forma clara cada aplicación / módulo / librería definidos y en algunos casos su ámbito de uso a la hora de elegir si algo se tiene que utilizar o no y sobre todo el como hacerlo.

Ejemplos :

  • acme-architecture-testing
  • acme-architecture-spring-testing
  • acme-api-greeting
  • acme-api-greeting-model
  • acme-api-greeting-config
  • ...

Convenciones de nomenclatura de paquetes

Para este tipo se ha seguido el siguiente formato :

{TIPO} . {COMPAÑIA} . {AMBITO / PROYECTO / APLICACION} . {MODULO}+? . {CARACTERISTICA}+? . {FUNCIONALIDAD}+? . {SUBFUNCIONALIDAD}+?

Detalle :

  • {TIPO} : texto puede ser un país o bien una organización
  • {COMPAÑIA} : texto que representa el nombre de la compañía propietaria
  • {AMBITO / PROYECTO / APLICACION} : texto del ámbito o bien del proyecto
  • {MODULO} (opcional): texto que representa el nombre de un módulo (Ejemplo : common, testing, security, core, web, report, rest, …)
  • {CARACTERISTICA} (opcional): texto que representa una característica concreta
  • {FUNCIONALIDAD} (opcional): texto que representa una tipología de clases (Ejemplo : constant, entity, enumerate, exception, factory, repository / dao, service, validation, util, ...)
  • {SUBFUNCIONALIDAD} (opcional): texto que representa una tipología o subtipología de clases (usa las anteriores)

Cuando mejor será la estructuración de los paquetes, mejor se hará el desarrollo de los diferentes componentes, así como facilitará la búsqueda y el cumplimiento del principio de PSR.

  • Enfoque Package-By-Feature

Ejemplos :

  • com.acme.example.archunit.constant
  • com.acme.architecture.testing.archunit.util
  • com.acme.architecture.testing.archunit.condition
  • com.acme.greeting.api.model.greeting.request
  • ...

Convenciones de nomenclatura de clases

Nota

Prácticamente la ubicación de una clase dentro de un paquete debería de determinar la funcionalidad que tendrá la clase

Para este tipo se ha seguido el siguiente formato :

[{PREFIJO_TIPO}]?{NOMBRE}{SUFIJO_TIPO}

Detalle :

  • {PREFIJO_TIPO} (opcional) : texto de prefijo que ayuda a determinar el tipo de clase a nivel de ciertos matices (Ejemplo : "Dummy", etc.)
  • {NOMBRE} : texto que identifica la clase
    • Normalmente lleva implícito el dominio funcional u objeto sobre el que aplicará
  • {SUFIJO_TIPO} : texto de sufijo que ayuda a determinar el tipo de clase (Ejemplo : "Service", "Util, "Repository", etc.)

Ejemplos :

  • ExampleNameConstant
  • MessageRepository
  • UserServiceImpl
  • ListConverterUtil
  • GenericException

2. Diseño de las Reglas Arquitectónicas

drawing Photo by Markus Spiske on Unsplash

En este apartado se mostrarán cuales han sido las premisas consideras para diseñar e implementar las Reglas Arquitectónicas utilizadas en los desarrollos de ejemplo, basadas sobre todo en el uso de Archunit y en lo que se quiere validar.

2.1. Aspectos a cubrir con las Reglas Arquitectónicas

Aspectos a cubrir con las Reglas Arquitectónicas:

  • Convenciones de nomenclatura: paquetes, clases, métodos, etc.
  • Convenciones de codificación: atributos, métodos, privados, públicos, constructores, inicializadores, etc.
  • Características
  • Contenido : paquetes, clases, etc.
  • Dependencias / Relaciones : entre capas, entre paquetes, entre clases, etc.
  • Herencia
  • Uso de anotaciones -> segun ambito : persistencia, transaccionalidad , seguridad, custom, etc.
  • Existencia de ciclos de uso
  • Restricciones
  • ...

2.2. Premisas del diseño de las Reglas Arquitectónicas

Categorías :

  • 2.2.1. Premisas para la Capa API "Core"
  • 2.2.2. Premisas para la Capa API "Lang"
  • 2.2.3. Premisas para la Capa API "Library"

2.2.1. Premisas para la Capa API "Core"

  • El mecanismo de importación de clases por defecto será a nivel de clase de test
    • Cada clase de test definirá el paquete con el que trabajar
    • La gran mayoría de las clases de test harán referencia a la paquetería utilizada en la definición del proyecto, es decir, su paquete "básico" -> Ejemplo "com.acme.example.api"
    • Se facilitará el uso de carga de clases a nivel de método cuando sea necesario
  • El mecanismo de importación de clases incluirá opciones de configuración por defecto para excluir: Tests, JARs y Archives
  • Por defecto se buscará que la declaración de los métodos de validación de reglas de arquitectura de la clase de test se hagan mediante la anotación @ArchTest

Ejemplo de código "Clase de Test cumplimiento las premisas Core"

@AnalyzeClasses(packages = TestingArchUnitPackageConstant.PACKAGE_BASE, 
importOptions = {  
        ImportOption.DoNotIncludeTests.class, 
        ImportOption.DoNotIncludeJars.class, 
        ImportOption.DoNotIncludeArchives.class 
    }
)
public class ExampleArchitectureTest {

@ArchTest
public static final ArchRules example = ...  
 ...
}

Esta clase de test establecerá el paquete sobre el que trabajar y aplicará parámetros de configuración. En este caso excluirá: Clases de Test, Archives y JARs.

Posteriormente y gracias al uso de la anotación @ArchTest se podrán definir Reglas Arquitectónicas como atributos estáticos dentro de la clase.

Ejemplo de código "Clase de Test para validar las reglas definidas adhoc"

@AnalyzeClasses(packages = TestingArchUnitPackageConstant.PACKAGE_BASE, 
importOptions = {  
        ImportOption.DoNotIncludeJars.class, 
        ImportOption.DoNotIncludeArchives.class 
    }
)
public class CatalogConstantArchitectureRuleTest {

@ArchTest
public static final ArchRule example_1 = ...;

@ArchTest
public static final ArchRule example_2 = ...;

 ...
}

Esta clase de test establecerá el paquete sobre el que trabajar y aplicará parámetros de configuración. En este caso excluirá: Archives y JARs.

  • Por lo que se podría aplicar sobre ejemplos de clases definidas en el directorio "src/test/java" y dependiendo del caso realizando alguna modificación sobre el paquete de trabajo.

Posteriormente y gracias al uso de la anotación @ArchTest se podrán definir Reglas Arquitectónicas como atributos estáticos dentro de la clase. Siendo las reglas utilizadas en este ejemplo referenciadas de forma adhoc dentro de la misma clase, es decir, que se definen en ese mismo sitio.

2.2.2. Premisas para la Capa API "Lang"

  • Uso de clases externas e individuales con función de catálogo (colección) de reglas ArchRule
    • Se ha creado una tipología de clases propia con la forma "Catalog*ArchitectureRule"
  • Uso de un enfoque donde cada clase catálogo esta enfocada sobre el componente de desarrollo o tipo de clase con el que trabajar
    • Por ejemplo : Catálogo para trabajar con clases del tipo Constante
  • Cada clase de catálogo cubrirá diferentes aspectos según las necesidades en sus ArchRules (nomenclatura, pertenencia a un paquete concreto, implementación, uso de anotaciones, etc.)
  • Las reglas ArchRule definidas dentro de un catálogo se implementarán con :
    • La anotación @ArchTest
    • Atributos estáticos a la clase
    • Una nomenclatura descriptiva de lo que hace cada regla arquitectónica
    • Cada regla se encargará de tener una única responsabilidad
    • El uso de la implementación de ArchRule basada en los atributos de ArchRuleDefinition
    • Se usarán Condiciones ("ArchCondition") y Predicados ("DescribedPredicate") personalizados y específicos en caso de ser necesario
  • El uso de catálogos favorece :
    • La creación de clases de reglas específicas
    • La creación de clases de reglas por composición con el objetivo de cubrir necesidades específicas de varios conceptos
    • La creación de grupos de clases de reglas
    • La ejecución completa de un propio catálogo como una unidad o bien en combinación con otros catálogos
    • La creación de un catálogo all-in-one (similar a un super grupo)
    • Su uso en diferentes arquitecturas
    • ...

Diagrama de representación : "Definición de Catálogos de Reglas Arquitectónicas"

Ejemplo de código "Catálogo de Reglas para Constantes cumplimiento las premisas Lang"

public class CatalogConstantArchitectureRule {

@ArchTest
public static final ArchRule constant_classes_should_be_in_constant_package =  
classes()  
.that().haveSimpleNameEndingWith(ArchUnitNameConstant.SUFFIX_NAME_CONSTANT_CLASS)
.should().resideInAPackage(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_CONSTANT_CLASS);

@ArchTest
public static final ArchRule constant_classes_should_have_names_ending_with_the_word_constant =  
classes()  
.that().resideInAPackage(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_CONSTANT_CLASS)
.should().haveSimpleNameEndingWith(ArchUnitNameConstant.SUFFIX_NAME_CONSTANT_CLASS);

@ArchTest
public static final ArchRule constant_classes_should_be_public =  
classes()  
.that().resideInAPackage(ArchUnitPackageConstant.RESIDE_PACKAGE_CONSTANT_CLASS)
.should().bePublic();
 ...
}

Esta clase de catálogo definirá el conjunto de reglas a aplicar sobre el tipo "Constante", para ello utilizará la anotación @ArchTest con la que se podrán definir Reglas Arquitectónicas como atributos estáticos dentro de la clase y según su validación deberían de establecer un identificador descriptivo

Ejemplo de código "Clase de Test para validar las reglas definidas de forma externa"

@AnalyzeClasses(packages = TestingArchUnitPackageConstant.PACKAGE_EXAMPLE, 
    importOptions = { 
            ImportOption.DoNotIncludeArchives.class, 
            ImportOption.DoNotIncludeJars.class 
    }
)
// Includes test classes
public class CatalogConstantArchitectureRuleTest {

@ArchTest
public static final ArchRule constant_classes_should_be_in_constant_package = CatalogConstantArchitectureRule.constant_classes_should_be_in_constant_package;

@ArchTest
public static final ArchRule constant_classes_should_have_names_ending_with_the_word_constant = CatalogConstantArchitectureRule.constant_classes_should_have_names_ending_with_the_word_constant;  
 ...
}    

Esta clase de test establecerá el paquete sobre el que trabajar y aplicará parámetros de configuración. En este caso excluirá: Clases de Test, Archives y JARs.

Posteriormente y gracias al uso de la anotación @ArchTest se podrán definir Reglas Arquitectónicas como atributos estáticos dentro de la clase. Siendo las reglas utilizadas en este ejemplo referenciadas de forma externa desde otra clase.

Diagrama de representación : "Definición de Grupos de Catálogos de Reglas Arquitectónicas"

Ejemplo de código "Clase de Test para validar las reglas definidas de forma externa en un grupo"

public class CatalogCoreGlobalArchitectureRule {

@ArchTest
public static final ArchRules base_constant_architecture = ArchRules.in(CatalogConstantArchitectureRule.class);

@ArchTest
public static final ArchRules base_annotation_architecture = ArchRules.in(CatalogCustomAnnotationArchitectureRule.class);

@ArchTest
public static final ArchRules base_data_factory_architecture = ArchRules.in(CatalogDataFactoryArchitectureRule.class);  
...
}

Ejemplo similar al anterior pero con la posibilidad de ejecutar catálogos completos de reglas tratándolos como si de una sola regla se tratara.

Ejemplo de código "Clase de Test para validar las reglas definidas de forma combinada"

public class CatalogSpringServiceArchitectureRule {

    ...
@ArchTest
public static final ArchRule spring_service_interface_classes_should_only_be_accessed_by_controllers_or_other_services_impl = CatalogServiceArchitectureRule.service_interface_classes_should_only_be_accessed_by_controllers_or_other_services_impl;

@ArchTest
public static final ArchRule spring_service_interface_classes_should_not_be_annotated_with_service =  
classes()    .that().resideInAPackage(SpringArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_SPRING_SERVICE_CLASS)   .should().notBeAnnotatedWith(Service.class);  
 ...
}

Ejemplo similar a los anteriores pero con la posibilidad de ejecutar combinados de catálogos diferentes de reglas, grupos y/o reglas adhoc.

2.2.3. Premisas para la Capa API "Library"

  • Uso de clases externas e individuales con función de catálogo donde cubrir la evaluación de arquitecturas basadas en capas
    • Se ha creado una tipología de clases propia con la forma "Catalog*LayeredArchitectureRule"
  • *Uso de un enfoque donde cada clase de evaluación de capas puede estar enfocada sobre un tipo de organización, estructura, distribución y/o otro enfoque de las capas a analizar
  • Cada clase de evaluación de capas proporciona sus propias ArchRules
    • Puede contener una o más reglas
  • Se podran utilizar indentificadores y elementos creados adhoc para su implementación
  • Las reglas ArchRule definidas dentro se implementarán con :
    • layeredArchitecture()
    • uso de capas -> .layer("XXX").definedBy("YYY")
    • uso de condiciones de las capas -> .whereLayer("XXX")
  • El uso de catálogos favorece :
    • La creación de clases de evaluación de capas específicas o bien reutilizables
    • La creación de agrupaciones de reglas de evaluación de capas
    • Su uso en diferentes arquitecturas
    • ...

Diagrama de representación : "Definición de Catálogos de Evaluación de Capas de Arquitectura"

Ejemplo de código "Clase de Test para evaluar las clases"

public class CatalogModelLayeredArchitectureRule {

    @ArchTest
    public static final ArchRule model_layered_architecture_should_have_a_default_definition = 
    layeredArchitecture()


    // **************
    // *** Layers ***
    // **************

    // Constants
    .layer(ArchUnitLayeredArchitectureConstant.CONSTANT_LAYER).definedBy(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_CONSTANT_CLASS)

    // DTOs
    .layer(ArchUnitLayeredArchitectureConstant.REQUEST_DTO_LAYER).definedBy(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_REQUEST_DTO_CLASS)
    .layer(ArchUnitLayeredArchitectureConstant.RESPONSE_DTO_LAYER).definedBy(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_RESPONSE_DTO_CLASS)
    .layer(ArchUnitLayeredArchitectureConstant.QUERY_REQUEST_DTO_LAYER).definedBy(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_QUERY_REQUEST_DTO_CLASS)

    // Others
    .layer(ArchUnitLayeredArchitectureConstant.DUMMY_LAYER).definedBy(ArchUnitPackageConstant.RESIDE_FINAL_PACKAGE_DUMMY_CLASS)
    .layer(ArchUnitLayeredArchitectureConstant.UTIL_LAYER).definedBy(ArchUnitPackageConstant.RESIDE_PACKAGE_UTIL_CLASS)


    // ******************
    // *** Conditions ***
    // ******************

    // Constants
    //  "Constant layer" should be accessible to all (no condition is established)

    // DTOs
    .whereLayer(ArchUnitLayeredArchitectureConstant.REQUEST_DTO_LAYER).mayOnlyBeAccessedByLayers(
            ArchUnitLayeredArchitectureConstant.DUMMY_LAYER, ArchUnitLayeredArchitectureConstant.UTIL_LAYER
    )
    .whereLayer(ArchUnitLayeredArchitectureConstant.RESPONSE_DTO_LAYER).mayOnlyBeAccessedByLayers(
            ArchUnitLayeredArchitectureConstant.DUMMY_LAYER, ArchUnitLayeredArchitectureConstant.UTIL_LAYER
    )
    .whereLayer(ArchUnitLayeredArchitectureConstant.QUERY_REQUEST_DTO_LAYER).mayOnlyBeAccessedByLayers(
            ArchUnitLayeredArchitectureConstant.QUERY_REQUEST_DTO_LAYER, ArchUnitLayeredArchitectureConstant.DUMMY_LAYER, ArchUnitLayeredArchitectureConstant.UTIL_LAYER
    )

    // Others
    //  "Dummy layer"
    //      Option 1 :  should be accessible to all (no condition is established)
    //      Option 2 :  should not be accessed by anyone in this context project (beware of scanning JARs)
    //                  .whereLayer("Dummy layer").mayNotBeAccessedByAnyLayer()
    .whereLayer(ArchUnitLayeredArchitectureConstant.UTIL_LAYER).mayOnlyBeAccessedByLayers(
            ArchUnitLayeredArchitectureConstant.UTIL_LAYER
    );

}

Este ejemplo define una serie de capas disponibles en la tipología de proyectos "modelo", donde se indentifica la capa, la ubicación y las condiciones sobre las que aplica.

En este caso todos esos valores vienen dados por diferentes tipos de clases de constantes.

3. Diseño de librerías de Arquitectura para el soporte de ArchUnit

drawing Photo by Yancy Min on Unsplash

Para ello definiremos dos librerías de arquitectura que proporcionen el soporte de las reglas arquitectónicas.

  • 3.1. Librería "acme-architecture-testing"
  • 3.2. Librería "acme-architecture-spring-testing"

3.1. Librería "acme-architecture-testing"

Librería de arquitectura personalizada Java pensada para cubrir los aspectos relacionados con el testing unitario / integración "típico" y con el soporte de reglas arquitectura para cubrir un Arquitectura de N-capas de proposito general.

Definirá reglas arquitectónicas para cubrir de forma general :

  • Entidades
  • DTOs (incluidas variantes)
  • Repositorios
  • Servicios
  • Controladores
  • Utilidades (incluidas variantes)
  • Factorías
  • Mapeadores
  • Constantes
  • Excepciones
  • Dummies
  • Anotaciones Custom (Si se usarán en la arquitectura)
  • ...

Tambien se ha incluido soporte a :

  • ArchRule, Predicados y Condiciones de Archunit
  • MapStructs

Importante

No se ha podido dar soporte a Lombok porque son anotaciones del tipo @Retention(SOURCE) y se generán después de compilar :

Se genera el siguiente error:

Caused by: com.tngtech.archunit.base.ArchUnitException$InvalidSyntaxUsageException: Annotation type lombok.Getter has @Retention(SOURCE), thus the information is gone after compile. So checking this with ArchUnit is useless.

Cada catálogo se definirá para cada tipo de clase e incluirá las Reglas Arquitectónicas más importantes consideradas para cada una de ellas.

Se han definido grupos de catálogos de forma que se pueda validar una arquitectura de forma genérica con grupos.

Se ha incluido un catálogo de capas de arquitectura para proyectos del tipo modelo y API por defecto.

IMPORTANTE

Normalmente una librería como esta, tendría definidas todas las dependencias individualmente con el objetivo de cubrir el ámbito de testing que se quiera cubrir o que venga determinado por las funcionalidades, es decir, se darían de alta las dependencias necesarias y de forma específica.

En este caso particular, se ha decidido que esta librería se utilice para aplicar testing y reglas arquitectónicas sobre una arquitectura y aplicaciones basadas en Spring.

Para facilitar el grado de compatibilidad de las tecnologías y versiones utilizadas con Spring se ha preferido utilizar la "propuesta" que realiza el propio starter de Spring para testing (spring-boot-starter-test). Por esto, se ha incluido spring-boot-starter-test como dependencias del proyecto teniendo en cuenta las cosas buenas y malas de incluir este tipo de librerías:

  • Muchas dependencias transitivas extras
  • Incremento del peso de los proyectos
  • Acoplamiento a determinadas tecnologías / versiones
  • Alineamiento frente a otros proyectos / librerías cuando usen Spring -> Asegurar el uso de la misma versión
  • Compatibilidad entre módulos de Spring
  • ...

El propio starter "spring-boot-starter-test" se compone NO solo del soporte de testing para Spring sino que incluye una serie de dependencias de tecnologías para testing (Junit, Mockito, Hamcrest, AssertJ, JSONassert, JsonPath, etc) por lo que NO solo ser podrá probar algo que dependa de Spring

En concreto y como pasa siempre con Spring, hay que apostar por una determinada versión a utilizar. Como el desarrollo que planteare utilizará Spring Boot he elegido la versión 2.3.4.RELEASE y esta "impone" el uso de los módulos de Spring en versión 5.2.9.RELEASE (aunque algunas versiones podrían variar)

Cada catálogo se definirá para cada tipo de clase de Spring e incluirá las Reglas Arquitectónicas más importantes consideradas para cada una de ellas.

Se han definido grupos de catálogos de forma que se pueda validar una arquitectura de forma genérica con grupos.

Se ha incluido un catálogo de capas de arquitectura para proyectos del tipo modelo y API por defecto.

Aspectos a tener en cuenta :

  • Uso en las aplicaciones como dependencia JAR
  • Uso de la dependencia interna de "spring-boot-starter-test" para probar en este caso cosas que NO dependen de Spring, con el objetivo de que haya una compatibilidad del 100% cuando realmente sea necesaria su uso
    • Esta librería establece por defecto una serie de dependencias con : Junit, Mockito, Hamcrest, AssertJ, JSONassert and JsonPath
    • Se va a excluir el soporte a JUnit4 para forzar el uso de JUnit5
  • Incorpora la dependencia de ArchUnit con soporte para JUnit5
  • Incopora las dependencias de tecnologías que serían interesantes de validar dentro de la arquitectura como : lombok y mapstruct
  • Incluye todas las Reglas Arquitectónicas (Types), Grupos (Type Groups) y los componentes definidos para cubrir una arquitectura N-capas () de propósito general
  • Esta librería debería de contener aquellos elementos o componentes necesarios para ayudar o mejorar el testing
  • Algunos de los componentes deberían de estar muy bien pensados previamente
  • Las reglas arquitectónicas han sido probadas en su ámbito de test con un juego de datos similar al utilizado en las aplicaciones
  • ...

Se ha definido un repositorio de código específico para esta librería.

Ejemplo de la representación de la librería desde un IDE

3.2. Librería "acme-architecture-spring-testing"

Librería de arquitectura personalizada Java pensada para cubrir los aspectos relacionados con el testing unitario / integración "típico" y con el soporte de reglas arquitectura para cubrir un Arquitectura de N-capas de propósito general basada en Spring que incluye persistencia en base de datos con JPA.

IMPORTANTE

Requiere utilizar como dependencia la librería "acme-architecture-testing"

Definirá reglas arquitectónicas para cubrir de forma general Spring con :

  • Entidades (cuando se quiere trabajar con persistencia por ejemplo de JPA)
  • Repositorios
  • Servicios
  • Controladores
  • Configuradores
  • Componentes
  • ...

Aspectos a tener en cuenta :

  • Uso en las aplicaciones como dependencia JAR
  • Uso de la dependencia interna de arquitectura "acme-architecture-testing"
  • Uso de la dependencia interna de "spring-boot-starter-web" NO para su uso sino para poder tener reglas arquitectónicas que lo utilicen
  • Uso de la dependencia interna de "spring-boot-starter-data-jpa" NO para su uso sino para poder tener reglas arquitectónicas que lo utilicen
  • Incluye todas las Reglas Arquitectónicas (Types), Grupos (Type Groups) y los componentes definidos para cubrir una arquitectura N-capas de propósito general basada en Spring (Layered)
  • Incluye Reglas Arquitectónicas (Types), Grupos (Type Groups) y los componentes para el desarrollo web con Spring y persistencia con JPA
  • Esta librería debería de contener aquellos elementos o componentes necesarios para ayudar o mejorar el testing
  • Algunos de los componentes deberían de estar muy bien pensados previamente
  • Las reglas arquitectónicas han sido probadas en su ámbito de test con un juego de datos similar al utilizado en las aplicaciones
  • ...

Se ha definido un repositorio de código específico para esta librería.

Ejemplo de la representación de la librería desde un IDE

4. Ejemplos de Uso

Las librerías anteriormente definidas aplicarán sobre los siguientes proyectos o aplicaciones

4.1. Aplicación "acme-api-greeting-model"

Proyecto que representa una librería relacionada con el modelo de negocio del servicio "Greeting" con el objetivo de centralizar la forma de comunicarse con el API, para ello incluye los diferentes tipos de Request y Response que pueda manejar el servicio API.

Para ello definirá diferentes DTOs según las necesidades : Request, Response, QueryRequest y FullQueryRequest

  • Se podría definir más de uno de cada tipo para cubrir diferentes requerimientos

Planteamiento de las librerías *-model

Este tipo de enfoques de librerías vienen muy bien cuando existe mucha comunicación con un microservicio ya que suele contestar a dos preguntas:

  • ¿Cómo te vas a comunicar "preguntando" al microservicio?
  • ¿Cómo se va comunicar "respondiendo" el microservicio?

Por regla general, este tipo de DTOs suelen estar implementadas en el propio microservicio, pero teniéndolos de forma externa se permite establecer el "contrato" de comunicaciones de forma centralizada por lo que será mas fácil invocar al microservicio desde otros microservicios.

Aspectos a tener en cuenta :

  • El proyecto representa una tipología concreta de proyectos -> *-model
  • Tienen un nombre según la nomenclatura establecida
  • Define un dominio funcional claro -> "Greeting"
  • Define un paquete de trabajo del proyecto -> "com.acme.greeting.api.model"
  • Hace uso de la dependencia "acme-architecture-testing" al NO depender de Spring
  • Usa las dependencias necesarias pasa ser compatible : jackson y lombok

Se ha definido un repositorio de código específico para esta librería.

Sobre este proyecto se ha aplicado la validación de arquitectura, para ello se han generado en el ámbito de test "src/test/java" dos paquete específicos :

Paquete "com.acme.greeting.api.model.greeting.archunit"

Se han ubicado ciertos tests de arquitectura :

  • ModelArchitectureTest : Test donde se aplican las reglas arquitectónicas definidas sobre un proyecto considerado "modelo"
    • Se establece el paquete de actuación como "com.acme.greeting.api.model.greeting"
    • Se aplican filtros sobre las clases con las que trabajar como : test, las clases de otras dependencias, etc.
    • Se aplica el grupo de catálogo global de la arquitectura genérica -> Enfoque Global

  • ModelLayeredArchitectureTest : Test donde se aplican las reglas de capas de funcionalidad definidas sobre un proyecto considerado "modelo"
    • Se establece el paquete de actuación como "com.acme.greeting.api.model.greeting"
    • Se aplican filtros sobre las clases con las que trabajar como : test, las clases de otras dependencias, etc.
    • Se aplica la regla de capas "Modelo" de la arquitectura genérica -> Enfoque Global

Paquete "com.acme.greeting.api.model.greeting.archunit.specific"

Se han ubicado ciertos tests de arquitectura :

  • ModelSpecificArchitectureTest : Test donde se aplican las reglas arquitectónicas definidas sobre un proyecto considerado "modelo"
    • Se establece el paquete de actuación como "com.acme.greeting.api.model.greeting"
    • Se aplican filtros sobre las clases con las que trabajar como : test, las clases de otras dependencias, etc.
    • Se aplica cada grupo de catálogo o reglas adhoc que se representan de forma particular -> Enfoque Específico

  • ModelSpecificLayeredArchitectureTest : Test donde se aplican las reglas de capas de funcionalidad definidas sobre un proyecto considerado "modelo"
    • Se establece el paquete de actuación como "com.acme.greeting.api.model.greeting"
    • Se aplican filtros sobre las clases con las que trabajar como : test, las clases de otras dependencias, etc.
    • Se aplica la regla de capas "Modelo" de la arquitectura genérica -> Enfoque Global
    • Se aplica las reglas de capa "Modelo" de forma particular -> Enfoque Específico

Ejemplo de "Pantalla de Ejecución completa de los Test para el proyecto acme-api-greeting-model desde Eclipse"

Ejemplo de "Pantalla de Ejecución completa de los Test para el proyecto acme-api-greeting-model desde Maven"

4.2. Aplicación "acme-api-greeting"

Proyecto que representa una "microservicio" (API REST reducido) sobre el modelo de negocio "Greeting"

Aspectos a tener en cuenta :

  • El proyecto representa una tipología concreta de proyectos -> Tipo API
  • Tienen un nombre según la nomenclatura establecida
  • Define un dominio funcional claro -> "Greeting"
  • Define un paquete de trabajo del proyecto -> "com.acme.greeting.api"
  • Hace uso de la dependencia "acme-architecture-spring-testing" al depender de Spring
  • Hace uso de la dependencia "acme-api-greeting-model" para utilizar su modelo de comunicaciones
  • Usa las dependencias necesarias pasa ser compatible : lombok, mapstruct, liquibase, h2, ...

Se ha definido un repositorio de código específico para esta librería.

Este proyecto dispone de test unitarios / integración para probar la funcionalidad.

Sobre este proyecto se ha aplicado la validación de arquitectura, para ello se han generado en el ámbito de test "src/test/java" dos paquete específicos de Archunit:

Paquete "com.acme.greeting.api.archunit"

Se han ubicado ciertos test de arquitectura :

  • ApiArchitectureTest : Test donde se aplican las reglas arquitectónicas definidas sobre un proyecto considerado "API"
    • Se establece el paquete de actuación como "com.acme.greeting.api"
    • Se aplican filtros sobre las clases con las que trabajar como : test, las clases de otras dependencias, etc.
    • Se aplica el grupo de catálogo global de arquitectura genérica
    • Se aplica el grupo de catálogo global de arquitectura de Spring
    • Se combinará la revisión de los dos catálogos de valdiaciones arquitectura -> Enfoque Global

  • ApiLayeredArchitectureTest : Test donde se aplican las reglas de capas de funcionalidad definidas sobre un proyecto considerado "API"
    • Se establece el paquete de actuación como "com.acme.greeting.api.model.greeting"
    • Se aplican el filtro de test sobre las clases con las que trabajar -> Permite incluir clases del paquete de modelo
    • Se aplica la regla de capas "API" de la arquitectura genérica Spring -> Enfoque Global

  • ShowFilesTest : Test utilizado para mostrar el número y las clases que serán importadas por Archunit
    • Hace uso de una clase de utilidades custom

Paquete "com.acme.greeting.api.archunit.specific"

Se han ubicado ciertos test de arquitectura :

  • ApiSpecificArchitectureTest : Test donde se aplica las reglas arquitectónicas definidas sobre un proyecto considerado "API"
    • Se establece el paquete de actuación como "com.acme.greeting.api"
    • Se aplican filtros sobre las clases con las que trabajar como : test, las clases de otras dependencias, etc.
    • Se aplica cada grupo de catálogo o reglas adhoc que se representan de forma particular -> Enfoque Específico

  • ApiSpecificLayeredArchitectureTest : Test donde se aplican las reglas de capas de funcionalidad definidas sobre un proyecto considerado "API"
    • Se establece el paquete de actuación como "com.acme.greeting.api"
    • Se aplican el filtro de test sobre las clases con las que trabajar -> Permite incluir clases del paquete de modelo
    • Se aplica las reglas de capa "API" de forma particular -> Enfoque Específico

Ejemplo de "Pantalla de Ejecución completa de los Test para el proyecto acme-api-greeting"

Ejemplo de "Pantalla de Ejecución completa de los Test para el proyecto acme-api-greeting desde Maven"

5. Conclusiones

Ya habéis visto que se puede validar la arquitectura de los proyectos Java con "cierta" facilidad e independencia de como estén montados. Bueno y con algo de conocimiento sobre arquitectura...jejeje

Lo más importante para poder hacerlo, es tener muy claro que es lo que se espera encontrar en la arquitectura y tener la capacidad para implementar la comprobación que lo haga. Sino habrá que presuponer que las cosas se hagan de una determinada manera e incluso hacer uso de aspectos propios del desarrollo como : clases abstractas, etc.

En desarrollos considerados "raros" normalmente se requieren validaciones "raras", esto no es malo si realmente es lo que nosotros queremos o necesitamos. Lo normal, es que una arquitectura sencilla e incluso compleja tenga validaciones desde muy básicas a las más complejas. Todo dependerá de hasta que nivel queramos validar si superficie o la zona más profunda. Lo que si que debemos tener claro es que para validar una arquitectura medianamente vamos a tener muchas validaciones de reglas de arquitectura.

Hemos podido "vivir en nuestras carnes" viendo los recursos de ejemplo, que de inicio se requiere cierto esfuerzo en aprender a utilizar Archunit, pensar/diseñar las reglas arquitectónicas, generar un juego de pruebas, probarlas y ver como utilizarlas en los proyectos.

Hemos visto que añade cierta complejidad a los proyectos y sobre todo añade más piezas de desarrollo que mantener en nuestro día a día. Pero también creo que el esfuerzo merece mucho la pena porque proporciona muchos beneficios :

  • "Tener claro lo que uno esta haciendo"
  • Diseño de arquitecturas con cierto sentido
  • Si realmente se aplican la validaciones se consiguen desarrollos estándares, normalizados y homogéneos
  • Detección temprana de incumplimientos de reglas arquitectónicas
  • Auditar el código de forma automática con cierta frecuencia
  • Minimizar problemas de diseño en los proyectos que aparecen con el día a día
  • Documenta de forma extra el código
  • Ayudar en los traspasos de conocimientos
  • Ayudar a formar a los nuevos desarrolladores
  • ...

En este artículo se proporcionan unas librerías que he tratado de que fueran lo más "completas" posibles (y a las que se les han dedicado unas cuantas horas con mucho "cariño") y lo más genéricas que sean posibles con vistas a que puedan ser reutilizadas.

Por lo que os quedan de recursos las librerías como posible punto de partida (evitando el problema del "folio en blanco"), para que así defináis vuestras propias reglas o bien vuestras propias librerías adaptadas a vuestras necesidades.

Y los recursos de las aplicaciones que al final parece que no, pero son dos proyectos que pueden ser también un buen punto de partida para hacer API REST con Spring.

Aun así existen muchas formas de hacer las cosas y se pueden hacer las reglas de validación de arquitectura como uno quiera...aquí aplica lo que siempre digo que "cada uno utilice aquello con lo que se sienta comodo siempre que funcione".

Ya me diréis que os han parecido los artículos y espero que os ayuden mucho :-)

Si os ha gustado la serie, no dudéis en seguirnos en Twitter para estar al día de próximos posts.

Víctor Madrid
Autor

Víctor Madrid

Líder Técnico de la Comunidad de Arquitectura de Soluciones en atSistemas. Aprendiz de mucho y maestro de nada. Técnico, artista y polifacético a partes iguales ;-)