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 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.
database , BillingDatabase :
@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 , BillingService . BillingService 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.