Uniendo componentes de arquitectura limpia con Spring Boot y ArchUnit

En el desarrollo de software, queremos crear "- columna vertebral ": Veo la espina dorsal , mantenibilidad columna vertebral , se extiende la columna vertebral , y - en una tendencia ahora - la descomposición (la capacidad de expandir el monolito en mikroservisy, si es necesario). Añádalo a la lista de su columna vertebral de habilidades favorita .

La mayoría, quizás incluso todas, de estas "características" van de la mano con dependencias puras entre componentes.

Si un componente depende de todos los demás componentes, no sabemos qué efectos secundarios tendrá el cambio de un componente, lo que dificulta el mantenimiento del código base y hace que sea aún más difícil extenderlo y descomponerlo.

Con el tiempo, los límites de los componentes del código base tienden a difuminarse. Aparecen malas dependencias, lo que dificulta el trabajo con el código. Esto tiene todo tipo de malas consecuencias. En particular, el desarrollo se está desacelerando.

Esto es aún más importante si estamos trabajando en una base de código monolítica que abarca muchas áreas diferentes de negocios, o "contextos delimitados" para usar la jerga del diseño basado en dominios.

¿Cómo podemos proteger nuestro código base de dependencias no deseadas? Con un diseño cuidadoso de contextos delimitados y adherencia constante a los límites de los componentes. Este artículo muestra un conjunto de prácticas que le ayudarán en ambos casos al trabajar con Spring Boot.

 Código de muestra

Este artículo va acompañado de un código de trabajo de muestra  en GitHub  .

Visibilidad privada del paquete

¿Qué ayuda a mantener los límites de los componentes? Visibilidad reducida.

Si usamos la visibilidad Package-Private para las clases "internas", solo las clases en el mismo paquete tendrán acceso. Esto dificulta la adición de dependencias no deseadas desde fuera del paquete.

, , .  ?

, .

, .

, , .

! , , . , , , .  !

, , package-private , , , .

?  package-private .  , package-private , , ArchUnit , package-private .

. , , :

.  .

Domain-Driven Design (DDD): , .  , .  «» « » .

, .  .

: , .  .  public , , .

API

, :

billing
├── api
└── internal
    ├── batchjob
    |   └── internal
    └── database
        ├── api
        └── internal

 internal, , , ,  api, , , API, .

 internal api :

  • .

  • ,  internal .

  • ,  internal .

  • api internal ArchUnit (  ).

  •   api  internal, , - .

,  internal package-private.  public ( public, ), .

, Java package-private , , .

.

Package-Private

 database:

database
├── api
|   ├── + LineItem
|   ├── + ReadLineItems
|   └── + WriteLineItems
└── internal
    └── o BillingDatabase

+, public, o, package-private.

database API  ReadLineItems WriteLineItems, , .  LineItem API.

 databaseBillingDatabase :

@Component
class BillingDatabase implements WriteLineItems, ReadLineItems {
  ...
}

, .

, .

 api,  internal, .   internal , ,  api.

 database, , , .

 batchjob:

batchjob API .   LoadInvoiceDataBatchJob(, , ), ,  WriteLineItems:

@Component
@RequiredArgsConstructor
class LoadInvoiceDataBatchJob {

  private final WriteLineItems writeLineItems;

  @Scheduled(fixedRate = 5000)
  void loadDataFromBillingSystem() {
    ...
    writeLineItems.saveLineItems(items);
  }
}

,  @Scheduled Spring,  .

,  billing:

billing
├── api
|   ├── + Invoice
|   └── + InvoiceCalculator
└── internal
    ├── batchjob
    ├── database
    └── o BillingService

billing InvoiceCalculator  Invoice.  ,  InvoiceCalculator ,  BillingServiceBillingService  ReadLineItemsAPI - :

@Component
@RequiredArgsConstructor
class BillingService implements InvoiceCalculator {

  private final ReadLineItems readLineItems;

  @Override
  public Invoice calculateInvoice(
        Long userId, 
        LocalDate fromDate, 
        LocalDate toDate) {
    
    List<LineItem> items = readLineItems.getLineItemsForUser(
      userId, 
      fromDate, 
      toDate);
    ... 
  }
}

, , , .

Spring Boot

, Spring Java Config  Configuration  internal   :

billing
└── internal
    ├── batchjob
    |   └── internal
    |       └── o BillingBatchJobConfiguration
    ├── database
    |   └── internal
    |       └── o BillingDatabaseConfiguration
    └── o BillingConfiguration

Spring Spring .

database :

@Configuration
@EnableJpaRepositories
@ComponentScan
class BillingDatabaseConfiguration {

}

@Configuration Spring, , Spring .

@ComponentScan Spring, ,  ,   ( )  @Component .   BillingDatabase, .

@ComponentScan  @Bean  @Configuration.

 database Spring Data JPA.  @EnableJpaRepositories.

batchjob  :

@Configuration
@EnableScheduling
@ComponentScan
class BillingBatchJobConfiguration {

}

@EnableScheduling.  , @Scheduled bean-LoadInvoiceDataBatchJob.

,  billing :

@Configuration
@ComponentScan
class BillingConfiguration {

}

@ComponentScan ,  @Configuration Spring bean-.

, Spring .

, ,  @Configuration. , :

  • ()   SpringBootTest.

  • () ,   @Conditional... .

  • , , () , () .

:  billing.internal.database.api public,  billing, .

, ArchUnit.

ArchUnit

ArchUnit - , .  , , .

,  internal .  ,  billing.internal.*.api  billing.internal.

 internal , - «».

( «internal» ), ,  @InternalPackage:

@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InternalPackage {

}

 package-info.java :

@InternalPackage
package io.reflectoring.boundaries.billing.internal.database.internal;

import io.reflectoring.boundaries.InternalPackage;

, , .

, , :

class InternalPackageTests {

  private static final String BASE_PACKAGE = "io.reflectoring";
  private final JavaClasses analyzedClasses = 
      new ClassFileImporter().importPackages(BASE_PACKAGE);

  @Test
  void internalPackagesAreNotAccessedFromOutside() throws IOException {

    List<String> internalPackages = internalPackages(BASE_PACKAGE);

    for (String internalPackage : internalPackages) {
      assertPackageIsNotAccessedFromOutside(internalPackage);
    }
  }

  private List<String> internalPackages(String basePackage) {
    Reflections reflections = new Reflections(basePackage);
    return reflections.getTypesAnnotatedWith(InternalPackage.class).stream()
        .map(c -> c.getPackage().getName())
        .collect(Collectors.toList());
  }

  void assertPackageIsNotAccessedFromOutside(String internalPackage) {
    noClasses()
        .that()
        .resideOutsideOfPackage(packageMatcher(internalPackage))
        .should()
        .dependOnClassesThat()
        .resideInAPackage(packageMatcher(internalPackage))
        .check(analyzedClasses);
  }

  private String packageMatcher(String fullyQualifiedPackage) {
    return fullyQualifiedPackage + "..";
  }
}

 internalPackages(), reflection ,  @InternalPackage.

 assertPackageIsNotAccessedFromOutside().  API- ArchUnit, DSL, , «, , , ».

, - public  .

: , (io.reflectoring ) ?

, ( ) io.reflectoring.  , .

, .

, :

class InternalPackageTests {

  private static final String BASE_PACKAGE = "io.reflectoring";

  @Test
  void internalPackagesAreNotAccessedFromOutside() throws IOException {

    // make it refactoring-safe in case we're renaming the base package
    assertPackageExists(BASE_PACKAGE);

    List<String> internalPackages = internalPackages(BASE_PACKAGE);

    for (String internalPackage : internalPackages) {
      // make it refactoring-safe in case we're renaming the internal package
      assertPackageIsNotAccessedFromOutside(internalPackage);
    }
  }

  void assertPackageExists(String packageName) {
    assertThat(analyzedClasses.containPackage(packageName))
        .as("package %s exists", packageName)
        .isTrue();
  }

  private List<String> internalPackages(String basePackage) {
    ...
  }

  void assertPackageIsNotAccessedFromOutside(String internalPackage) {
    ...
  }
}

 assertPackageExists() ArchUnit, , , .

.  , , .  ,  @InternalPackage  internalPackages().

, .

Java- Spring Boot ArchUnit , - .

API , .

!

, ,  GitHub .

Spring Boot,   moduliths.




All Articles