全部版块 我的主页
论坛 数据科学与人工智能 数据分析与数据科学 python论坛
739 0
2025-12-12

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 中两种典型的模型设计方式——“贫血模型”与“充血模型”,分析它们在实际应用中的适用场景、优缺点以及选择策略。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

相关推荐
栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群