15 逃离“大泥球”:分层架构的边界控制与依赖管理
欢迎进入第15讲。在上一节中,我们深入探讨了经典四层架构的核心理念,明确了每一层所承担的关键职责。这就像获得了一份详尽的城市规划蓝图——我们知道中央商务区对应的是领域层,交通枢纽是应用层,而住宅区则类比为表现层。
然而,即便拥有再完美的设计图,若实际开发过程中不按规范执行,最终构建出的系统仍可能沦为一片混乱的“棚户区”。在真实的项目实践中,这样的“违章施工”屡见不鲜:
- 为了省事,在 Controller(表现层)中直接调用 MyBatis 的 Mapper 接口访问数据库(基础设施层)。
- 在一个纯粹的 Entity 类(领域层)里,竟然引入了外部框架或工具类(基础设施层)。
- 两个本应独立的业务模块,因共享一小段通用逻辑而产生不必要的耦合。
这些看似微不足道的“便捷操作”,随着时间推移,会不断侵蚀原本清晰的层级边界。层与层之间、模块与模块之间的依赖关系逐渐错乱,最终导致整个系统变成一个难以维护、难以理解的“大泥球(Big Ball of Mud)”。
所谓“大泥球”,是软件工程中用来形容那些缺乏明确架构、结构混乱系统的术语。在这种系统中进行修改,常常如履薄冰,牵一发而动全身。
那么,如何从代码层面真正守护我们的分层架构,确保其在长期迭代和多人协作中始终保持清晰?如何避免系统滑向“大泥球”的命运?
本讲将聚焦于分层架构的“执法机制”,重点讲解如何通过严格的依赖规则和自动化手段,来维持代码架构的健康与整洁。
一、架构的根基:依赖规则
在所有架构设计原则中,有一条被视为“第一性原理”的核心准则——依赖规则(The Dependency Rule)。这一概念由 Robert C. Martin(Bob 大叔)在其“整洁架构(Clean Architecture)”理论中提出,但其思想广泛适用于各类分层架构模型,包括 DDD 的四层结构。
依赖规则指出:源代码中的所有依赖方向,必须指向内部——即从低层策略指向高层策略。
这里的“高层策略”指的是系统中最核心、最稳定的部分。在 DDD 中,这正是领域层。它承载着业务规则与领域模型,是系统存在的根本依据。
而“低层策略”则是指技术实现细节,容易随环境变化的部分,例如基础设施层和表现层。无论是使用 MySQL 还是 Postgres 作为数据库,或是采用 Vue 还是 React 构建前端界面,这些都属于可替换的技术细节。
因此,在 DDD 四层架构中,该规则体现为:
- 表现层、应用层、基础设施层可以依赖领域层;
- 但领域层绝不能反向依赖任何其他层。
所有的依赖箭头,最终都应汇聚并指向中心的领域层。
虽然这条规则看起来简单,但它却是构建“洁净架构”的基石。它的价值在于保障了领域模型的独立性与稳定性。当核心业务逻辑不绑定于任何具体技术时,它就具备了可移植性、可独立测试性和长期演进的能力。我们可以自由更换外层组件(如数据库驱动、UI 框架),而无需改动核心逻辑。
二、边界破坏者:常见的违规依赖
要保护领域层的纯洁性,最大的威胁来自于那些看似“不可避免”的底层依赖。其中最具代表性的就是数据持久化需求。
当领域逻辑执行完毕后,通常需要将聚合根的状态保存到数据库中。这就涉及到与数据库的交互。如果不加控制,很容易出现以下情况:
Order
聚合根直接依赖某个具体的仓储实现,而该实现又依赖 MyBatis 或 JPA 等框架。这样一来,依赖链变成了:
Domain -> Infrastructure
这明显违背了依赖规则——领域层被基础设施层“污染”了。
如何切断这条非法的依赖路径?答案来自 Bob 大叔提出的另一关键原则:依赖倒置原则(Dependency Inversion Principle, DIP)。
三、解决方案:依赖倒置原则的应用
依赖倒置原则(DIP)强调:
- 高层模块不应依赖低层模块,二者都应依赖抽象。
- 抽象不应依赖细节,细节应依赖抽象。
听起来有些抽象?我们通过图示来说明:
错误的做法:
graph TD
A[应用层<br>OrderApplicationService] --> B[领域层<br>Order];
B --> C[基础设施层<br>OrderRepositoryImpl];
note for C "领域层依赖了具体实现!"
在这种方式中,领域层直接依赖了基础设施层的具体实现类,违反了依赖规则。
正确的做法(应用依赖倒置后):
graph TD
subgraph "应用层"
App[OrderApplicationService]
end
subgraph "领域层"
Domain[Order]
RepoInterface[<b>IOrderRepository (抽象接口)</b>]
end
subgraph "基础设施层"
RepoImpl[OrderRepositoryImpl]
end
App --> RepoInterface;
Domain -- "可以看作也依赖" --> RepoInterface;
RepoImpl -- "<b>实现</b>" --> RepoInterface;
note for RepoInterface "高层和低层,都依赖于这个抽象接口!"
我们成功地将依赖方向进行了“反转”。
具体发生了什么变化?
- 我们在领域层定义了一个抽象的
IOrderRepository
接口。这个接口是由高层(领域层)向低层(基础设施层)提出的能力契约,表达了对持久化功能的需求。
- 领域层和应用层仅依赖此抽象接口,不涉及任何具体实现。
- 在基础设施层中,编写
OrderRepositoryImpl
类,实现领域层定义的接口。
通过这个抽象接口作为桥梁,我们实现了依赖关系的“倒置”。现在,不再是领域层依赖基础设施,而是基础设施层(细节)反过来依赖领域层(抽象),完全符合依赖规则的要求。
依赖倒置原则,堪称守护分层架构边界的“核武器”。
它不仅适用于Repository
场景,还可以推广至消息队列、第三方服务调用、文件存储等多种外部依赖的解耦设计中。只要坚持“高层定义接口,底层实现接口”的模式,就能有效隔离变化,保持核心逻辑的纯净与稳定。
autowire
RedisTemplate
OrderRepositoryImpl
该原则不仅适用于特定场景,也广泛适用于所有领域层需要与外部系统进行交互的情况。例如:
当领域层需要发布一个领域事件时——应在领域层中定义相应的接口,具体实现则交由基础设施层通过 Kafka 或 RabbitMQ 完成。
IDomainEventPublisher
当领域层需要调用外部系统的防腐层时——同样在领域层定义接口规范,实际的通信逻辑由基础设施层借助 HTTP Client 实现。
IAntiCorruptionLayer
三、代码层面的“边界巡警”:架构守护测试
理论清晰易懂,但在实际开发中,项目结构复杂、团队成员技术水平不一。如何确保某位新入职的开发者不会为了图方便,直接在领域层代码中引入违规依赖?
new
比如,误引入了某个基础设施或表现层的类。
RedisTemplate
虽然口头提醒和 Code Review 能起到一定作用,但它们具有主观性和偶然性,难以持续保障架构一致性。我们需要一种更加可靠、自动化的机制——
一个永不疲倦的“边界巡警”,能够实时监控代码结构,一旦发现违反架构规则的行为,立即触发警告甚至阻断流程。
这个角色,正是架构守护测试(Architecture Guardian Test)所承担的职责。
借助静态代码分析工具或专用测试框架,我们可以编写出用于验证架构约束的自动化测试用例。在 Java 生态中,最具代表性的解决方案之一是 ArchUnit。
使用 ArchUnit 强化架构约束
ArchUnit 是一个开源、轻量且功能强大的 Java 测试库,支持以链式、流式的 API 风格编写对系统架构的断言检查。
以下是两个典型的应用示例:
1. 维护领域层的“纯净性”
规则设定:领域层(即位于 ..domain.. 包路径下的类)不得依赖应用层、表现层或基础设施层。
domain
application
presentation
infrastructure
可通过以下 ArchUnit 测试代码实现强制校验:
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
public class ArchitectureTest {
private final JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.project");
@Test
public void domain_layer_should_only_depend_on_itself() {
ArchRule rule = layeredArchitecture()
.layer("Domain").definedBy("..domain..")
.layer("Application").definedBy("..application..")
.layer("Presentation").definedBy("..presentation..")
.layer("Infrastructure").definedBy("..infrastructure..")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure", "Presentation")
.whereLayer("Application").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("Domain").shouldOnlyDependOnLayers("Domain");
rule.check(importedClasses);
}
}
若任何位于
domain
包中的类,
import
引用了
application
或
infrastructure
包下的组件,该测试将失败,进而中断 CI/CD 构建流程,防止问题代码合入主干。
2. 确保“依赖倒置原则”落地
规则要求:所有仓库(Repository)的实现类必须置于基础设施层,并严格实现领域层中定义的接口。
Repository
infrastructure
domain
对应的 ArchUnit 校验逻辑如下:
@Test
public void repositories_should_follow_dependency_inversion() {
ArchRule rule = classes()
.that().haveNameMatching(".*RepositoryImpl")
.should().resideInAPackage("..infrastructure.repository..")
.andShould().implementInterfacesThat().resideInAPackage("..domain.repository..");
rule.check(importedClasses);
}
此类测试作为构建流程的一部分,能有效防止架构腐化,确保高层模块不依赖低层模块,二者都依赖于抽象,从而提升系统的可维护性与扩展性。
守护领域模型的“纯洁性”
领域模型是整个系统的核心,其稳定性与独立性至关重要。为了确保其不受外部框架影响,必须保证其不依赖于具体技术实现,例如 Spring 框架的相关组件。
为此,可以制定如下规则:
位于 ..domain.model.. 包下的类,不应引入或使用任何来自 org.springframework.. 的类或注解。这能有效避免在实体或值对象上误用如
@Component
、
@Autowired
等 Spring 特定注解,从而保护领域层的纯粹性。
通过 ArchUnit 编写自动化测试,可对上述规则进行验证:
@Test
public void domain_models_should_not_use_spring_annotations() {
ArchRule rule = noClasses()
.that().resideInAPackage("..domain.model..")
.should().dependOnClassesThat().resideInAPackage("org.springframework..");
rule.check(importedClasses);
}
Entity
Value Object
分层架构中的依赖规范
在基础设施层(infrastructure)中,持久化相关的类需要遵循特定结构:它们应存在于 ..infrastructure.persistence.. 包下,并实现名称符合 .*Repository 模式的接口。
这一约束可通过以下 ArchUnit 测试来强制执行:
ArchRule rule = classes()
.that().resideInAPackage("..infrastructure.persistence..")
.and().areNotInterfaces()
.should().implement(classWithNameMatching(".*Repository"));
rule.check(importedClasses);
domain.model
从设计到执行:让架构真正落地
优秀的软件架构不仅需要精心的设计,更需要严格的执行与持续的维护。仅靠文档和会议无法保障架构的一致性,唯有将规则固化为可运行的检测机制,才能实现真正的“令行禁止”。
实现这一目标的关键要素包括:
- 一条核心原则:依赖方向必须指向内层。外层模块可以依赖内层,但内层绝不能反向依赖外层。领域层作为最核心的部分,应当保持最高级别的抽象与稳定。
- 一个关键模式:依赖倒置原则(DIP)。通过定义接口,使高层模块依赖抽象而非具体实现,低层模块则去实现这些抽象接口,从而反转传统依赖关系,增强系统的灵活性与可测试性。
- 一项自动化手段:架构守护测试。借助工具如 ArchUnit,将架构约定转化为自动化的单元测试,集成至 CI/CD 流水线中,实现全天候监控。
当这些实践被全面落实后,你便完成了从“架构设计师”到“架构执行官”的转变。你不仅能描绘理想的系统蓝图,更能确保每一行代码都符合既定结构,让系统像一座规划有序的城市般稳健发展,远离“大泥球”式的混乱演化。
后续内容预告
下一讲我们将深入探讨 DDD 中两种典型的模型设计方式——“贫血模型”与“充血模型”,分析它们在实际应用中的适用场景、优缺点以及选择策略。