🌐 中文 English
引言
在软件系统日益复杂化的今天,我们经常会遇到这样的痛点:业务逻辑被频繁变更的产品需求折腾得支离破碎,代码结构逐渐演变成难以维护的“大泥潭”。
从最初的简单传统单体架构到如今复杂的分布式和微服务架构,软件架构的演变经历了多个阶段。而如何在高并发、微服务盛行的时代,优雅地划分服务边界、控制业务复杂度?DDD(领域驱动设计,Domain-Driven Design) 成为了我们打破僵局的终极利器。
软件架构的演进历程
为了更好地理解 DDD 的价值,我们首先来看看软件架构演变经历的三个典型阶段:
- 传统单体架构:所有的应用功能都集成在一个单一的应用程序中,所有模块和组件都在同一个进程内运行,请求直接操作数据库,甚至不进行代码分层。虽然易于早期开发和部署,但随着业务增长,不同功能的模块严重耦合,牵一发而动全身。
- MVC分层架构:应用被划分为不同的层(如controller、service、mapper等)。层与层之间通过接口进行交互,促进了模块化。但面对复杂业务场景,MVC架构的灵活性和可扩展性有限, 随着业务增长,service层会变得无比臃肿,维护成本逐渐增加。
- 微服务架构:将系统拆分为多个小而独立的服务,每个服务由独立的团队开发、部署和维护,通过轻量级协议(如 HTTP、RPC 或消息队列)进行通信。服务之间独立,易于横向扩展。
🚨 微服务拆分的“艺术”难题
随着微服务的普及,业界迎来了一个新的难题:服务拆分的粒度应该多大?边界如何定义?
- 拆分太细:项目复杂度过高,接口网络调用成本、分布式事务处理、服务运维成本大幅上升。
- 拆分太粗:业务边界变得模糊,服务之间依然高度耦合,失去了微服务的优势。
而 DDD 就是一套指导我们根据领域模型确定业务边界的方法论,从而划分出应用的边界,最终落实为服务边界和代码边界。
DDD 领域驱动设计核心概念
DDD 通过领域驱动设计方法定义领域模型,从而确定业务和应用的边界,保证业务模型和代码模型的一致性。
1. DDD 的三大目标
- 通过领域模型实现业务需求:让开发者与领域专家共同理解业务需求,形成共享语言并构建模型。
- 提高系统的灵活性与可维护性:通过合理划分限界上下文,减少系统的耦合度,使得不同模块可以独立演化。
- 支持复杂业务逻辑的表达:通过深入的业务建模,让复杂的业务逻辑能够清晰、准确地反映在代码中。
核心思想:让系统更贴合业务,让大型系统更利于独立建设和维护。
2. DDD 的两大建设阶段
DDD 的建设是一套组合拳,通常包含 战略设计 和 战术设计 两部分。
① 战略设计(面向业务:划定边界)
从业务出发,建立领域模型,统一限界上下文。设计时通常需要进行事件风暴(Event Storming):
- 现场还原:领域专家、架构师、开发、测试、产品和项目经理齐聚一堂,在白板上贴满五颜六色的贴纸,发散思维。
- 核心讨论:系统涉及哪些业务?哪个业务动作会触发另一个业务?输入输出是什么?
- 收敛边界:梳理领域对象之间的关系,进行聚类,形成聚合、聚合根、限界上下文,最终完成微服务的拆分。
在事件风暴中,通常会结合产品设计方法,如用例分析(描述系统与外部交互)、场景分析(探讨用户在不同环境下的使用)以及用户旅程分析(描绘用户从开始到结束的一系列步骤)。
② 战术设计(面向技术:代码落地)
从技术实现出发,将领域模型和代码模型进行映射。这个阶段主要负责完成代码落地,包括聚合、聚合根、实体、值对象等代码逻辑的设计与实现。
DDD 体系名词硬核解析
为了避免在实际编码中“鸡同鸭讲”,我们需要彻底搞懂 DDD 的核心术语:
| 核心名词 | 一句话定义 | 核心特征与代码映射 |
|---|---|---|
| 领域 (Domain) | 业务关注的问题空间与边界。 | 用来确定范围和边界。可进一步拆分为核心域(核心竞争力)、通用域(如支付)、支撑域(如网关)。 |
| 限界上下文 (Bounded Context) | 统一通用语言的业务语义环境。 | 定义了业务边界。例如:电商语义下叫“商品”,运输语义下叫“货物”,上下文确保团队内认知无歧义。 |
| 实体 (Entity) | 具有唯一标识(ID)的业务对象。 | 状态可变但 ID 不变。代码中通常采用充血模型(相关业务逻辑直接写在实体类中)。 |
| 值对象 (Value Object) | 没有唯一标识、只描述特征的对象。 | 状态不可变,修改时只能整体替换。例如:用户的“地址”属性。 |
| 聚合 (Aggregate) | 将多个实体和值对象组合成的整体。 | 高内聚低耦合的组织。是数据修改和持久化的基本单位,也是微服务拆分的最小单位。 |
| 聚合根 (Aggregate Root) | 聚合内的带头人,统一对外窗口。 | 聚合根也是实体,有 ID。外部不得直接访问聚合内的其他实体,必须通过聚合根提供的接口进行合作。 |
| 领域服务 (Domain Service) | 无法归属于单一实体的跨对象业务行为。 | 封装核心业务规则。例如:涉及订单、账户、支付等多实体交互的“订单支付”逻辑。 |
💡 动态视角看世界:实体和值对象并不是一成不变的。对电脑主机来说,显卡是一个值对象(坏了就换新);但对于显卡厂商来说,显卡是有出厂编号、需要全程追踪的实体。
DDD 架构设计:充血模型 VS 贫血模型
在落地战术设计时,如何组织领域对象的数据和行为,是区分传统开发与 DDD 开发的分水岭。这本质上是对领域对象中 “数据与行为的职责划分” 的不同理解,主要分为贫血模型和充血模型。
| 维度 | 贫血模型 (Anemic Domain Model) | 充血模型 (Rich Domain Model) |
|---|---|---|
| 核心定义 | 领域对象仅包含数据(属性)与 getter/setter,不包含业务逻辑。 | 领域对象不仅包含数据,还包含处理这些数据的业务逻辑。 |
| 对象角色 | 纯粹的“数据容器”,只有状态,没有行为。 | 真正的“面向对象”,既有状态,又有行为。 |
| 逻辑归属 | 所有的业务逻辑都剥离在单独的服务类中(如 Service 层)。 | 属于该对象自身的业务逻辑直接内聚在对象内部。 |
| 适用场景 | 适合简单业务(如传统的 CRUD 系统)。 | 适合复杂业务,让业务逻辑和数据紧密结合。 |
四、DDD 经典四层架构演进
在理解了充血模型之后,如何为这些富有生命力的领域对象搭建舞台?这就是 DDD 的四层分层架构模型。它通过将应用划分为职责清晰的四个层次,从而促进代码的高内聚、低耦合与高可维护性。
1. DDD 四层架构硬核拆解
① 用户接口层 (User Interface Layer)
- 定位:也叫表示层或 Web 层,主要负责与外部(用户、其他系统的 API、前端页面)进行交互。
- 职责:接收用户输入并返回系统输出。该层绝不包含任何业务逻辑,主要负责参数校验(JSR303)、鉴权、以及将用户的请求数据(DTO)转发到应用层。
② 应用层 (Application Layer)
- 定位:整个系统的“外交官”与“总导演”,主要用来协调和编排。
- 职责:应用层本身不包含任何核心业务规则,它通过调用领域层的实体/领域服务,以及基础设施层的资源(如发送邮件、MQ消息)来实现特定的业务用例。同时,分布式事务控制(
@Transactional)、安全权限校验、外部远程 RPC 调用也都落在此层。
③ 领域层 (Domain Layer)
- 定位:整个架构的绝对核心与灵魂,纯粹内聚业务逻辑。
- 职责:包含了应用的核心业务规则、策略和状态流转。前面提到的聚合根、实体、值对象、领域服务统统安家于此。它的设计完全与底层技术(如数据库、缓存)脱耦,只为了将真实的业务需求精准转化为代码。
④ 基础设施层 (Infrastructure Layer)
- 定位:为上层提供底层的技术支持与通用服务。
- 职责:负责与外部系统(如 MySQL、Redis、RabbitMQ)进行物理交互。提供数据持久化实现、日志记录、邮件发送等工具。
2. 严格分层 VS 松散分层
在分层架构的调用关系中,业界通常存在两种流派:
- 严格分层架构 (Strict Layering):每层只能与直接下层产生依赖。即:
用户接口层->应用层->领域层->基础设施层。这种结构最为稳健,每一层都是天然的沙箱,解耦最彻底。 - 松散分层架构 (Relaxed Layering):层与层之间的交互更加灵活。例如,用户接口层或应用层可以绕过领域层,直接调用基础设施层去读取底层的报表数据。这种方式在快速开发、应对纯查询(CQRS)场景时非常高效,但随着系统复杂度增加,容易导致依赖混乱。
Byolio 避坑指南(依赖倒置原则 DIP): 传统的开发习惯是上层依赖下层,导致领域层严重绑定了数据库。 DDD 的基础设施层采用了依赖倒置设计(Dependency Inversion Principle)。domain层只定义持久化的接口规范(如
OrderRepository接口),而基础设施层去负责编写这个接口的具体实现(如OrderRepositoryImpl)。 这样一来,整个核心领域层就变成了一个不依赖任何具体框架的纯净 Java 模块,哪怕后续把 MySQL 换成 MongoDB,也只需要重写基础设施层的实现,核心业务逻辑稳如泰山。
3. 从传统三层架构到 DDD 四层架构的转化
对于熟悉 SpringBoot 开发的同学来说,我们最习惯的是三层架构(Controller-Service-Dao)。下面这张图和代码结构,直观地展示了它们是如何映射并演进到 DDD 四层架构的:
📐 职责与概念映射对照
| 传统三层架构 (MVC) | DDD 四层架构 | 核心转化逻辑 |
|---|---|---|
| Controller 层 (表示层) | User Interface 层 (用户接口层) | 职责基本对齐,负责 HTTP 请求接入、DTO 校验及响应组装。 |
| Service 层 (业务逻辑层) | Application 层 + Domain 层 | 核心拆分点! 传统的万能 Service 被一分为二:流程编排和第三方对接留给 Application,核心业务规则沉淀进 Domain 的实体和领域服务中。 |
| DAO / Repository 层 | Infrastructure 层 (基础设施层) | 传统 Dao 只管写 SQL。DDD 的基础设施层不仅管数据库(实现 Domain 定义的 Repository 接口),还接管了 Redis、MQ 等所有技术细节。 |
📁 落地到 SpringBoot 的包结构演变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 传统三层MVC架构
com.byolio.project
├── controller // 各种 Controller
├── service // 臃肿的业务逻辑,面向过程
│ └── impl
├── model // entity 对象 / vo 对象 / dto 对象
└── mapper // MyBatis Mapper
---------------- 演进为 ----------------
// DDD 四层架构 (单体模块包结构示例)
com.byolio.project
├── interfaces // 1. 用户接口层
│ ├── assembler // DTO 转换器,负责将请求参数转换为领域层的实体。
│ ├── controller // Controller层
│ ├── vo // vo对象
│ └── dto // 数据传输对象,负责请求参数校验和响应组装。
|
├── application // 2. 应用层 (极薄,只负责组装和编排)
│ └── service // 跨domain的业务逻辑
|
├── domain // 3. 领域层 (核心,纯 Java,不依赖具体技术框架)
│ ├── order // 按聚合划分限界上下文
│ │ ├── entity // 充血模型实体 (聚合根)
│ │ ├── valobject // 值对象
│ │ ├── repository // 纯接口定义!不写实现
│ │ └── service // 领域服务
|
├── infrastructure // 4. 基础设施层 (具体的技术实现)
│ ├── config // 配置类
│ ├── common // 公共组件
│ ├── api // API 接口
│ ├── mapper // 数据库mapper层
│ ├── manager // 基础设施通用组件
│ ├── utils // 工具类
│ └── repository // 核心!实现 domain 层定义的 Repository 接口
关于 shared(共享域)的引入: 很多同学在刚写 DDD 时会陷入一个死理——既然要求强隔离,那每个聚合就必须完全独立。结果导致类似于 Money(金额值对象)、BaseEntity(包含通用 ID 和时间字段的基类)在 order、user、product 等各个包里被重复写了三四遍。 shared 包就是为了打破这种“死板隔离”而生的。它存放的是整个系统达成高度共识的、无业务歧义的底层通用领域概念。 但必须警惕 shared 变成新的大泥潭!只有那些不带具体业务特异性的对象(如通用的异常定义、纯粹的通用值对象)才可以放进 shared。如果把 OrderEntity 的某一部分也因为图省事抽到 shared 里供其他聚合直接依赖,那就彻底破坏了限界上下文的边界,全盘皆输。牢记:宁可适度重复,也不要盲目共享业务。
DDD的缺点
虽然 DDD 能够完美应对复杂业务,但它绝不是银弹。在实际落地中,它也存在着不可忽视的痛点:
- 学习曲线极其陡峭 DDD 引入了大量抽象的概念(如限界上下文、聚合根、充血模型、值对象等)。团队成员需要彻底扭转过去那种“CRUD / 数据库表驱动”的传统思维模式,思维转型的成本极高。
- “牵一发而动全身”的字段修改痛点 在传统的“贫血模型 + 数据库驱动”开发中,如果表结构需要新增一个字段,往往只需要修改底层的 Entity 和顶层的 DTO,数据就能一路“透传”。 但在 DDD 严格的分层和防腐机制下,由于引入了大量的对象隔离,底层表结构仅仅增加一个字段,就可能需要同步修改 4、5 个对象定义以及对应的 Mapper 转换器。这种极高的改动成本,在频繁变动的敏捷开发初期会让人非常抓狂。
- 极易流于形式,架空 Domain 层(形式主义 DDD) 这是国内团队在推行 DDD 时最常见的“翻车”现场。由于研发人员根深蒂固的“面向过程”和“Service 狂魔”习惯,在实际编码时,大家往往贪图快,直接在最顶层的 Application Service(应用服务层)中把所有业务逻辑写完,而下层的 Domain 实体和领域服务则沦为了毫无作用的“传话筒”和空壳。 这种“外表四层架构,内核依然贫血”的形式主义 DDD,不仅没有享受到对象高内聚的好处,反而白白增加了好几层代码调用的工作量,成了名副其实的“架构包袱”。
FAQ
为什么不同公司的DDD架构会不同?
这是很多刚看完技术博客、转头去翻看公司企业级源码的同学最普遍的困惑。答案很简单:DDD 是一套设计思想(Methodology),而不是一套生搬硬套的代码规范(Framework)。
具体落地的代码架构长成什么样,往往取决于以下三个核心维度的权衡与妥协:
1. 团队对“严格分层”与“工程效率”的妥协
- 严格分层:在一些超大型金融、支付、核心电商团队,他们会严格遵守隔离原则。UI 层和领域层甚至在不同的 Maven Multi-Module(多模块) 结构里。为了防止任何一点污染,他们宁愿写大量的
Converter,一条字段要转 4、5 次手。 - 工程效率:很多中小型团队或快速迭代的业务为了“工程效率”,会采用单模块下的包隔离。他们甚至会在查询(CQRS)场景下,允许
interfaces层通过松散分层,绕过领域层直达底层mapper。因为他们知道,天天改字段如果转 5 次手,团队开发效率会被直接拖垮。
2. “单体”与“微服务”所处的生命周期不同
- 单体演进阶段:如果系统目前还是个大单体,划分包结构时,往往会设置一个
shared共享包。因为在同一个进程内,共享一些基础资产能够极大地减少重复代码。 - 微服务成熟阶段:如果团队已经将各个聚合拆成了物理隔离的微服务,那么他们基本会砍掉大而全的
shared包。因为微服务之间强调的是彻底解耦和零依赖。哪怕两个微服务里有 80% 相似的Money值对象,他们也宁愿在各自的服务里 Copy 一份,以此换取服务独立部署、独立演进的能力。
3. 康威定律的影响:组织架构决定代码架构
“设计系统的组织其产生的设计等同于组织之内的沟通结构。” —— 康威定律(Conway’s Law)
每个公司的业务复杂度、甚至团队的人员架构都是不一样的:
- 有的公司业务极其复杂、且频繁变更(如复杂的供应链系统),他们就会把
domain层设计得非常厚、非常重,重兵把守。 - 有的公司虽然叫推行 DDD,但实际上 90% 都是统计报表和 CRUD 业务,他们的
domain层自然就会沦为一层空壳,甚至整个架构更偏向传统的贫血模型,只是套了个四层的皮。
byolio 聊重要架构观: 学 DDD,千万不要去抄某个开源项目的代码骨架就觉得拿到了真传。我们要学的是它“让技术迎合业务”的内核。 在工程落地中,“没有最好的架构,只有最适合当前业务阶段和团队现状的妥协”。根据业务的痛点去剪裁和定制属于你们团队自己的包结构,才是真正的领域驱动设计。
总结
DDD 不是一种具体的框架,而是一种让技术回归业务本质的设计思想。通过战略设计,我们可以在宏观上理清复杂的业务边界;通过战术设计,我们可以利用实体、聚合根等工具在微观上写出更具表达力、更健壮的代码。
在面对长周期、跨部门协作、业务复杂的长期维护项目时,DDD 能够赋予系统极强的生命力和演化能力。