在开发软件时,我们想创建“ -spine ”:我看到了spine,可维护性spine,spine扩展,以及-目前的趋势-分解(如果需要,可以在mikroservisy上扩展整体功能)。添加到您最喜欢的'能力脊椎列表。“
这些“功能”中的大多数(甚至可能全部)与组件之间的纯依赖关系并驾齐驱。
如果一个组件依赖于所有其他组件,那么我们不知道更改一个组件会有什么副作用,这使得维护代码库变得困难,并且扩展和分解变得更加困难。
随着时间的流逝,代码库中组件的边界趋于模糊。不良的依存关系出现,这使得使用代码变得更加困难。这会带来各种不良后果。特别是,发展正在放缓。
如果我们正在处理跨越许多不同业务领域或“有限上下文”的整体代码库,以使用域驱动设计术语,则这一点尤为重要。
我们如何保护我们的代码库免受不必要的依赖? 精心设计有限的上下文并始终遵守组件边界。 本文演示了一套在两种情况下都可以在使用Spring Boot时为您提供帮助的实践。
样例代码
本文随附GitHub上的示例工作代码 。
包私有可见性
什么有助于保持组件边界?可见度降低。
如果我们对“内部”类使用Package-Private可见性,则只有同一包中的类才可以访问。 这使得很难从程序包外部添加不需要的依赖项。
, , . ?
, .
, .
, , .
! , , . , , , . !
, , 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.