DDD领域驱动设计
开篇:为什么大厂都在推 DDD?
先想一个问题:你有没有遇到过这样的代码——Service 层有几千行,一个方法里塞了查库、校验、计算、调用外部接口、发消息等各种逻辑,改一行代码就怕影响十行?
这就是传统 MVC 架构的典型痛点:业务逻辑全部散落在 Service 层。项目初期觉得简单直接,但随着业务复杂度增长,Service 变成了一个大杂烩——既负责流程编排,又负责业务规则,还负责数据转换。你想改一个计费规则,得翻遍整个 Service 才能确认改全了没有。
DDD(Domain-Driven Design,领域驱动设计)的核心主张是:软件开发的核心是理解业务,而不是实现技术。 先理解业务领域、建立领域模型,再考虑技术实现——而不是上来就画表、写 SQL。
传统开发的顺序是:需求 -> 设计表结构 -> 写 CRUD -> 在 Service 里堆业务逻辑。
DDD 的顺序是:需求 -> 领域建模(实体、值对象、聚合、领域事件)-> 最后才考虑持久化存储。
这个顺序的差异看似微小,却决定了代码的组织方式和长期的可维护性。
DDD 不是一个框架,也不是一套工具——它是一种思维方式和方法论。
一、DDD 核心概念
DDD 的概念不少,第一次接触可能会觉得术语多到头疼。但核心的只有这几个。我们用一个电商订单系统来串联理解。
1.1 领域 (Domain) 和子域 (Subdomain)
领域就是你的系统要解决的业务范围。 一个电商系统的领域就是"电商"。这个领域太大了,不可能一口吃下,需要拆成更小的子域来管理:
- 核心域 (Core Domain):直接决定竞争力的部分。电商系统的核心域是"交易"——下单、支付、履约。这里投入最多资源、最精心建模。
- 支撑子域 (Supporting Subdomain):不是核心竞争力,但核心域离不开它。比如"商品管理"、"库存管理"。
- 通用子域 (Generic Subdomain):与行业无关的通用能力。比如"用户认证"、"消息通知"、"文件服务"。
划分子域的意义在于:不同的子域,投入的资源和设计精细度应该不同。 核心域值得精心建模和反复推敲;通用子域用现成方案就好,没必要重复造轮子。
1.2 限界上下文 (Bounded Context)
限界上下文是 DDD 里最重要、也是最容易被误解的概念。
简单说:限界上下文是一个明确的业务边界,在这个边界内,所有概念和术语都有唯一、确定的含义。
同一个词在不同的限界上下文中,含义可能完全不同。比如"商品"这个词:
- 在商品上下文中,商品是有名称、描述、图片、类目的信息实体。
- 在库存上下文中,商品是一个有 SKU、实际库存、安全库存的库存单元。
- 在交易上下文中,商品是一个有单价、优惠规则的订单行项目。
如果你把这些含义全揉在一个 Product 类里,这个类就会膨胀到不可维护——字段多到你不知道哪些是什么场景用的。限界上下文的作用就是让每个上下文里的模型保持简单、聚焦。
一般来说,每个限界上下文对应一个微服务。这也是 DDD 和微服务架构天然契合的原因。
1.3 实体 (Entity)
实体是有唯一标识的对象,并且有自己的生命周期。 即使两个实体的其他属性完全一样,只要标识不同,它们就是不同的实体。
生活中的例子:两个叫"张三"的人,名字一样、年龄一样,但身份证号不同——它们是两个不同的实体。
代码中的例子:一个订单从创建、支付、发货到完成,经历了多次状态变化——但订单号始终不变,它始终是同一个实体。
关键特征:有唯一标识、可变(属性会随时间变化)、有生命周期(创建到销毁)。
在数据库中,实体通常对应一行记录,唯一标识就是主键。
1.4 值对象 (Value Object)
值对象是没有唯一标识的对象,它通过自身的属性来描述某种特征。 值对象是不可变的——一旦创建就不能修改,只能用一个新的值对象替换它。
生活中的例子:一个"地址"——它由省、市、区、街道组成。你不会说"这是第 3 号地址",你只会说"北京市朝阳区xx街道"。如果两个地址的所有字段都一样,那它们就是"相等"的——不需要比较 ID。
代码中的例子:"价格"——原价 129 元,活动价 99 元。它们只是两个不同的数值描述,没有 ID,没有生命周期。想改价格?不是在原来的值对象上修改,而是创建一个新的值对象。
关键特征:无唯一标识、不可变、通过属性相等判断相等。
在数据库中,值对象通常不需要单独的表——它可以作为实体的一个 JSON 字段存储,或者嵌入到实体对应的表中。
1.5 聚合 (Aggregate) 和聚合根 (Aggregate Root)
聚合是一组有内在关联的对象的集合,它们在业务上构成一个整体。 聚合根是这个整体的"代言人"——外部只能通过聚合根来操作聚合内的对象。
判断是否构成聚合的关键问题:当整体不存在时,部分是否还有意义?
- 订单不存在了,订单明细还有什么意义?没有。所以"订单"和"订单明细"构成一个聚合,"订单"是聚合根。
- 商品下架了,商品评论还有意义吗?有争议——如果你认为评论可以独立存在(比如展示历史评论),那评论就不应该是商品聚合的一部分。
聚合的规则:
- 外部只能引用聚合根,不能直接引用聚合内部的实体或值对象。
- 聚合根负责保证内部所有对象的一致性约束。
- 聚合内的对象与聚合根有相同的生命周期——聚合根被删除,内部对象也随之删除。
1.6 领域事件 (Domain Event)
领域事件是领域模型内部发生的、值得关注的事情。 比如"订单已创建"、"订单已支付"、"库存已扣减"。
领域事件的最大作用是解耦。当一个订单被创建时,需要做的事情可能有很多:扣减库存、生成支付单、发送通知、记录日志。如果把这些逻辑全写在下单接口里,下单接口会越来越臃肿,改一个地方就牵一发动全身。
用领域事件的方式:下单接口只管创建订单并发布 OrderCreatedEvent。库存服务订阅这个事件来扣减库存,通知服务订阅这个事件来发短信,支付服务订阅这个事件来生成支付单——各自独立,互不干扰。
发布者不关心谁在订阅,订阅者不关心谁在发布——这就是松耦合。
领域事件一般在单个微服务内部传递(通过 Spring Event 等机制),和 MQ 的消息不完全是一回事。但跨微服务的领域事件通知,最有效的方式恰恰就是通过消息队列来实现。
二、战略设计
战略设计是 DDD 的"宏观层面"——在写代码之前,先把业务领域的大图画清楚。
2.1 统一语言 (Ubiquitous Language)
DDD 特别强调一件事:开发团队和业务团队必须使用同一套语言来描述业务概念。
到底叫"商品价格"还是"商品单价"还是"商品售价"?叫"退款"还是"逆向交易"?不要小看这种命名差异——如果业务说"退款"开发写的是 reverseTransaction,沟通成本会越来越高,需求理解的偏差也会越来越大。
统一语言不是写在文档里就完事了——它要贯穿到代码的类名、方法名、数据库字段名中。代码本身就是对业务语言最精确的表达。
2.2 上下文映射 (Context Mapping)
当你划分好多个限界上下文后,它们之间不可能完全独立——总有需要交互的地方。上下文映射就是描述不同限界上下文之间的关系模式。
常见的关系类型:
- 共享内核 (Shared Kernel):两个上下文共享一部分模型。改动需要双方协调,耦合度较高。
- 客户-供应商 (Customer-Supplier):上游提供服务,下游消费。上游接口变更需要考虑下游影响。
- 防腐层 (Anti-Corruption Layer):当你依赖一个外部系统(或遗留系统),又不想被它的模型"污染"你的领域模型,就加一个翻译层——把外部的数据结构转换成你的领域模型。
防腐层在微服务架构中极其常用。比如你的订单服务调用第三方支付接口,不应该让第三方的数据结构侵入你的领域模型。在中间加一个 Feign 接口层做翻译——订单服务像调用本地方法一样调用支付能力,第三方接口变了只需要改防腐层,不影响业务代码。
2.3 战略建模实战:秒杀系统
以秒杀系统为例,战略建模分四步走。
第一步:划分领域模型层
第二步:划分核心域和非核心域
- 核心域:秒杀活动信息(活动场次、活动规则、状态管理)
- 支撑子域:商品信息(基本属性、销售属性)、库存信息(实际库存、销售库存)
- 通用子域:用户信息(账号、认证)
第三步:确定限界上下文及业务边界
- 活动上下文(核心域)
- 商品上下文、库存上下文(支撑子域)
- 用户上下文(通用子域)
第四步:识别领域对象,为战术建模做准备
| 类型 | 对象 |
|---|---|
| 实体 | 专题、商品、账号 |
| 值对象 | 实际库存、销售库存、原价、活动价 |
| 聚合根 | 场次、活动商品 |
| 领域事件 | 活动未开始、活动进行中、库存已售罄、活动已结束 |
三、战术设计
战术设计是 DDD 的"微观层面"——具体到每个限界上下文内部,怎么用代码组织领域模型。
3.1 贫血模型 vs 充血模型
这是 DDD 落地时最核心的代码组织选择。
贫血模型 (Anemic Domain Model):POJO 对象只有 get/set 方法,没有任何业务逻辑。所有业务逻辑都堆在 Service 层。这是传统 MVC 开发中最常见的做法。
// 贫血模型——商品只是一个数据容器
public class Product {
private String name;
private double price;
private int stock;
// 只有 getter/setter,没有业务逻辑
}
// 业务逻辑全在 Service 里
public class ProductService {
public void purchase(Product product, int quantity) {
if (quantity > product.getStock()) {
throw new IllegalArgumentException("库存不足");
}
product.setStock(product.getStock() - quantity);
}
}充血模型 (Rich Domain Model):领域对象自身包含业务逻辑。Service 层只负责编排流程(调用哪些领域对象、按什么顺序),不做具体的业务判断。
// 充血模型——商品自己知道怎么被购买
public class Product {
private String name;
private BigDecimal price;
private int stock;
public void purchase(int quantity) {
if (quantity > stock) {
throw new IllegalArgumentException("库存不足");
}
stock -= quantity;
}
}充血模型的优势: 业务逻辑和数据在一起,封装性好。改业务规则只需要改领域对象,不用翻遍整个 Service 层。领域对象自包含业务逻辑,更容易理解和测试。
贫血模型的优势: 简单直接,学习成本低,上手快。对象间的协作关系简单。适合业务逻辑简单、变更不频繁的系统。
充血模型的劣势: 需要对业务有深入理解才能建好模型,学习曲线较陡。对象间的协作可能增加,设计复杂度上升。
贫血模型的劣势: 业务逻辑散落在多个 Service 中,难以维护和理解。封装性差,容易出现耦合。
实际项目中怎么选?业务简单用贫血模型足矣。业务复杂(尤其是业务规则频繁变更的核心域)用充血模型更可持续。大部分大厂的核心业务系统都在往充血模型迁移。
3.2 工厂 (Factory) 和仓库 (Repository)
工厂负责创建复杂的领域对象——它是领域对象生命周期的起点。当一个对象的创建过程涉及多个数据源查询和数据装配时,就应该用工厂来封装创建逻辑。
比如要装载一个访客申请对象:
- 表单工厂分别调用表单 DAO、明细 DAO、用户 DAO 去查询。
- 把得到的明细对象和用户对象装配到表单信息对象中(set 到对应属性)。
- 返回一个完整的、可以直接使用的访客申请对象。
仓库负责领域对象的存取。它是领域层和基础设施层之间的桥梁——领域层只知道"把对象交给仓库保管"和"从仓库按 ID 取对象",不关心底层是 MySQL 还是 Redis 还是文件系统。
仓库的一个常见优化模式:
- 外部通过 ID 向仓库请求对象。
- 仓库先到缓存中查找——找到就直接返回。
- 没找到就通知工厂,工厂调用 DAO 查数据库、装配领域对象返回给仓库。
- 仓库在返回给调用方的同时,把对象放入缓存,下次就不用查数据库了。
如果服务器内存无限大,我们不需要任何数据库——所有领域对象都放在仓库(内存)里就行。数据库只是持久化的手段,不是领域模型的本质。
3.3 领域服务 (Domain Service)
当一个业务操作不自然地属于任何一个实体或值对象时,就应该放到领域服务中。
比如"转账"操作——它涉及两个账户实体(转出账户和转入账户),不适合放在任何一个账户实体里。这时候定义一个 TransferService 领域服务来承载这个逻辑,同时操作两个实体。
四、DDD 分层架构
DDD 的标准四层架构:
关键原则:
- 调用关系是从上到下的单向依赖——上层可以调用下层,绝不能反向调用。
- 领域层是核心,不依赖任何外部技术细节。它不知道你用的是 MySQL 还是 MongoDB,不知道你用的是 Spring 还是 Quarkus。
- 基础设施层通过实现领域层定义的接口(如 Repository 接口)来提供技术能力。这样更换技术栈时,只需要换基础设施层的实现,领域层完全不用动。
除了标准四层架构,DDD 还有两种流行变体:
- 洋葱架构 (Onion Architecture):从外到内层层包裹,像洋葱一样。依赖方向严格从外向内——最内层是领域模型,最外层是基础设施和 UI。
- 六边形架构 (Hexagonal Architecture):也叫端口-适配器架构。把系统看成一个六边形,每条边是一个端口(HTTP 输入端口、DB 输出端口等),通过适配器连接外部世界。
三种架构形式不同,但设计思想一致:以领域模型为中心,技术细节在外围。 这是 DDD 微服务架构高内聚低耦合原则的完美体现。
五、DDD 实战案例:电商订单领域建模
让我们用一个具体的例子来串联所有概念。
5.1 识别领域和限界上下文
电商系统的业务领域可以划分为三个核心领域:商品、订单、用户。
- 商品上下文:商品信息管理(名称、描述、价格、类目、图片)
- 订单上下文:订单的完整生命周期(创建 -> 支付 -> 发货 -> 完成 / 取消)
- 用户上下文:用户注册、登录、个人信息管理
5.2 建立统一语言
统一定义:商品的单价统一叫"商品价格"(而不是"商品单价"或"商品售价")。促销活动新增"促销价格"属性——需求变更时同步更新领域模型。
5.3 订单上下文的领域模型
- 实体: Order(有 orderId,有生命周期:待支付 -> 已支付 -> 已发货 -> 已完成)
- 值对象: Delivery(配送信息一旦确定就不变,通过属性描述,没有独立 ID)
- 聚合根: Order(外部通过 orderId 操作订单,不能直接操作 OrderItem)
- 领域事件: OrderCreatedEvent -> OrderPaidEvent -> OrderShippedEvent -> OrderCompletedEvent / OrderCanceledEvent
5.4 数据存储设计
值对象可以作为实体的属性直接存储(如用 JSON 字段),不需要单独建表:
| orderId | status | totalPrice | orderItems (JSON) |
|---|---|---|---|
| 20240101001 | CONFIRMED | 258.00 | [{"productName":"xxx", "quantity":2, "unitPrice":129}] |
取出数据时,将 JSON 反序列化成 OrderItem 对象列表。这样存储简单,查询也方便。OrderItem 就是值对象——他是依赖于 Order 实体存储的,没有独立的生命周期。
六、微服务落地实践
DDD 不只是理论——它和微服务架构有天然的契合。每个限界上下文对应一个微服务,微服务内高内聚,微服务间低耦合。
6.1 微服务内高内聚
每个微服务中的代码变化都是同一类原因引起的。因为这类原因而需要变更的代码都在这个微服务内,与其他微服务无关——独立修改、独立测试、独立发布。这才是微服务的真正优势。
6.2 微服务间低耦合
当一个微服务需要执行不属于自己职责的操作时,通过接口调用其他微服务,而不是自己去做。微服务之间通过 API 和事件通信,彼此当成黑盒。
关键实践:
- 数据隔离:用户信息表的读写只有用户微服务能做。其他微服务需要用户信息时必须通过 API 调用,禁止直接查表。
- 接口复用:面对多个团队的接口需求,通过复用尽可能少的接口满足它们。新需求优先看现有接口能否覆盖。
- 向前兼容:接口变更尽量向前兼容——不改名称和参数,只在内部增加新功能。宁愿新增一个接口也不要修改原有接口的签名。
- 防腐层:在本地增加一个 Feign 接口层作为防腐层。业务代码像调本地方法一样调用,对方微服务拆分变更导致接口地址变了,只需改防腐层。
6.3 数据库去中心化
不同微服务的数据特点不同,应该选择最合适的存储,而不是统一用一个大 MySQL:
- 数据量小但频繁读取的(用户信息、权限配置):小型 MySQL + Redis 前置缓存。
- 数据量大且高并发写的(通行记录、订单流水):TiDB 等 NewSQL 数据库做分布式存储。
- 海量历史数据的决策分析和秒级查询:NoSQL 或大数据平台,做宽表预处理后应对复杂查询。
七、常见面试题精选
面试题 1:DDD 的分层架构是怎样的?
标准四层从上到下:用户接口层、应用层、领域层、基础设施层。调用关系是从上到下的严格单向依赖。
除此之外还有洋葱架构(从外到内层层包裹)和六边形架构(端口 + 适配器模式)。三种形式不同,但设计思想一致:以领域模型为中心,技术细节在外围。
面试题 2:什么是充血模型和贫血模型?
贫血模型:对象只有 get/set,业务逻辑全在 Service 里。简单直接,适合小型项目。
充血模型:对象自身包含业务逻辑。封装性好,业务变更只需要改领域对象。适合复杂业务系统。
贫血模型的最大问题是业务逻辑分散——改一个业务规则要翻遍多个 Service。充血模型的最大挑战是需要深入理解业务才能建好模型。
面试题 3:什么是聚合和聚合根?
聚合是一组有内在关联的对象集合,在业务上构成一个整体。聚合根是聚合的唯一入口——外部只能通过聚合根操作聚合内部的对象。
判断标准:当整体不存在时,部分是否还有意义。订单不存在了,订单明细就没有意义——所以订单是聚合根,订单明细是聚合内部的实体。
面试题 4:DDD 的实现流程是什么?
六步走:
- 确定业务领域:明确业务问题和领域边界(商品、订单、用户)。
- 设计领域模型:识别实体、值对象、聚合、领域服务和领域事件。
- 建立统一语言:业务和开发使用同一套术语,贯穿到代码命名中。
- 实现领域模型:将模型转化为代码。聚合封装实体和值对象,领域服务处理跨实体操作,领域事件传递模块间消息。
- 设计应用架构:应用层编排流程,基础设施层实现持久化和外部服务调用,接口层对外暴露 API。
- 持续演进:确保模型和业务需求一致,需求变化时及时更新模型。
小结
DDD 的核心理念概括为三句话:
- 业务是根基:先理解业务再写代码。代码应该是业务概念的忠实映射——你的代码结构应该让一个业务人员也能大致看懂模块划分。
- 边界是关键:限界上下文划清业务边界,每个上下文内部保持简单、聚焦。跨越边界的通信通过接口和事件,不要让模型泄漏到边界外面。
- 模型是资产:领域模型不是一次性设计,而是随业务持续演进的核心资产。保持软件设计不退化的关键,在于每次需求变更时做出正确的设计——将变更还原到真实世界中,看看真实世界是什么样子,根据真实世界进行变更。
DDD 的学习曲线确实比 CRUD 陡峭。但对于复杂业务系统,它是保持代码长期可维护性的最有效方法。小步快跑,从简单问题开始逐步深入,让领域模型像小树一样一点一点成长——不要试图在项目第一天就设计出完美的领域模型。