Componentes de arquitetura limpa delimitadora com Spring Boot e ArchUnit

Quando desenvolvemos software, queremos criar "- coluna ": Vejo espinha , manutenção espinha , estender espinha , e - em uma tendência agora - decomposição (a capacidade de expandir o monólito em mikroservisy, se necessário). Adicione à lista de sua ' coluna de habilidade "favorita .

A maioria - talvez até todos - desses "recursos" andam de mãos dadas com dependências puras entre os componentes.

Se um componente depende de todos os outros componentes, não sabemos quais efeitos colaterais a alteração de um componente terá, o que torna difícil manter a base de código e torna ainda mais difícil estender e decompor.

Com o tempo, os limites dos componentes na base de código tendem a se confundir. Aparecem dependências ruins, tornando mais difícil trabalhar com o código. Isso tem todos os tipos de consequências ruins. Em particular, o desenvolvimento está desacelerando.

Isso é ainda mais importante se estivermos trabalhando em uma base de código monolítica que abrange muitas áreas de negócios diferentes ou "contextos limitados" para usar o jargão do Domain-Driven Design.

Como podemos proteger nossa base de cĂłdigo de dependĂŞncias indesejadas? Com design cuidadoso de contextos limitados e aderĂŞncia constante aos limites dos componentes. Este artigo demonstra um conjunto de práticas que o ajudam em ambos os casos ao trabalhar com o Spring Boot.

 CĂłdigo de amostra

Este artigo Ă© acompanhado por um exemplo de cĂłdigo de trabalho  no GitHub  .

Visibilidade privada do pacote

O que ajuda a manter os limites dos componentes? Visibilidade reduzida.

Se usarmos a visibilidade Package-Private para classes "internas", apenas as classes no mesmo pacote terĂŁo acesso. Isso torna difĂ­cil adicionar dependĂŞncias indesejadas de fora do pacote.

, , .  ?

, .

, .

, , .

! , , . , , , .  !

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