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
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
ReadLineItems
API - :
@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.