Membatasi komponen arsitektur yang bersih dengan Spring Boot dan ArchUnit

Ketika kita mengembangkan perangkat lunak, kami ingin membuat "- tulang belakang ": Saya melihat tulang belakang , pemeliharaan tulang belakang , memperpanjang tulang belakang , dan - dalam tren sekarang - dekomposisi (kemampuan untuk memperluas monolit pada mikroservisy, jika perlu). Tambahkan ke daftar ' tulang punggung kemampuan "favorit Anda .

Sebagian besar - bahkan mungkin semua - "fitur" ini sejalan dengan dependensi murni antar komponen.

Jika sebuah komponen bergantung pada semua komponen lainnya, kita tidak tahu efek samping apa yang akan ditimbulkan oleh perubahan satu komponen, yang membuatnya sulit untuk mempertahankan basis kode dan membuatnya semakin sulit untuk diperpanjang dan terurai.

Seiring waktu, batasan komponen dalam basis kode cenderung kabur. Dependensi yang buruk muncul, membuatnya lebih sulit untuk bekerja dengan kode. Ini memiliki segala macam konsekuensi buruk. Secara khusus, perkembangan melambat.

Ini semua menjadi lebih penting jika kita bekerja pada basis kode monolitik yang mencakup banyak area bisnis yang berbeda atau "konteks terbatas" untuk menggunakan jargon Desain Berdasarkan Domain.

Bagaimana kita melindungi basis kode kita dari ketergantungan yang tidak diinginkan? Dengan desain yang cermat dari konteks yang dibatasi dan kepatuhan yang konstan pada batas-batas komponen. Artikel ini menunjukkan serangkaian praktik yang membantu Anda dalam kedua kasus saat bekerja dengan Spring Boot.

 Kode sampel

Artikel ini disertai dengan contoh kode yang berfungsi  di GitHub  .

Visibilitas Paket-Pribadi

Apa yang membantu menjaga batasan komponen? Visibilitas berkurang.

Jika kita menggunakan visibilitas Package-Private untuk kelas "dalam", hanya kelas dalam paket yang sama yang akan memiliki akses. Ini membuatnya sulit untuk menambahkan dependensi yang tidak diinginkan dari luar paket.

, , .  ?

, .

, .

, , .

! , , . , , , .  !

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