Limiter les composants d'architecture propre avec Spring Boot et ArchUnit

Lorsque nous développons des logiciels, nous voulons créer des "- spine ": je vois la colonne vertébrale , la maintenabilité la colonne vertébrale , l'extension de la colonne vertébrale , et - dans une tendance actuelle - la décomposition (la possibilité d'étendre le monolithe sur mikroservisy, si nécessaire). Ajoutez à la liste de votre colonne vertébrale de capacité préférée . "

La plupart - peut-ĂȘtre mĂȘme la totalitĂ© - de ces «fonctionnalitĂ©s» vont de pair avec de pures dĂ©pendances entre les composants.

Si un composant dépend de tous les autres composants, nous ne savons pas quels seront les effets secondaires du changement d'un composant, ce qui rend difficile la maintenance de la base de code et rend encore plus difficile l'extension et la décomposition.

Au fil du temps, les limites des composants de la base de code ont tendance à s'estomper. De mauvaises dépendances apparaissent, ce qui rend plus difficile le travail avec le code. Cela a toutes sortes de mauvaises conséquences. En particulier, le développement ralentit.

Ceci est d'autant plus important si nous travaillons sur une base de code monolithique qui couvre de nombreux domaines d'activité différents ou des «contextes limités» pour utiliser le jargon de la conception pilotée par domaine.

Comment pouvons-nous protĂ©ger notre base de code des dĂ©pendances indĂ©sirables? Avec une conception soignĂ©e des contextes dĂ©limitĂ©s et une adhĂ©rence constante aux limites des composants. Cet article prĂ©sente un ensemble de pratiques qui vous aident dans les deux cas lorsque vous travaillez avec Spring Boot.

 Exemple de code

Cet article est accompagnĂ© d'un exemple de code de travail  sur GitHub  .

Visibilité privée du package

Qu'est-ce qui aide à garder les limites des composants? Visibilité réduite.

Si nous utilisons la visibilitĂ© Package-Private pour les classes "internes", seules les classes du mĂȘme package auront accĂšs. Cela rend difficile l'ajout de dĂ©pendances indĂ©sirables depuis l'extĂ©rieur du package.

, , .  ?

, .

, .

, , .

! , , . , , , .  !

, , 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