简介:这份资源是领域驱动设计(DDD)的官方示例代码,面向希望将 DDD 方法论落地到真实业务的中高级开发者与架构学习者。它以船运系统为背景,把领域模型、聚合、实体与值对象、领域事件、领域服务、限界上下文、仓储持久化等核心概念落到可运行的工程结构中,适合对照学习如何划分业务边界、组织分层架构与编写领域逻辑。压缩包共 426 个文件,约 23.57MB,以 142 个 Java 源码和 110 个 class 文件为主体,辅以 56 个 html 页面、33 个 xml 配置、35 个 jar 依赖及少量 jsp、properties、sql 等,覆盖表现层、应用层与基础设施层的典型组成。目前已有 1901 人学习下载。读者可从中获得一套结构完整的 DDD 工程范本,借助示例数据生成、货物跟踪与预订服务等模块理解聚合设计与仓储解耦思路,并参考其中的测试与配置组织方式,将相关模式迁移到自己的项目中。
1. 从一份“看得懂却跑不通”的示例代码说起
很多同学第一次接触 DDD,都是从一份官方示例代码开始的。下载、解压、打开 IDE,目录结构看着挺规整,聚合、实体、值对象、仓储、领域服务这些概念在代码里都能对上号,可一旦想把它改成自己的课程设计或者毕设题目,问题就来了:不知道哪些层该动、哪些边界不能碰,改着改着就退化成“贫血模型 + 一堆 Service”。这份领域驱动设计官方示例代码的价值,不在于让你照抄,而在于它给了一套可以拆解、可以对照、可以验收的骨架。它适合正在做 DDD 相关毕设或课程设计的学生,也适合要把一套分层架构落到实际交付里的开发者。下面我按“需求怎么立、结构怎么分、模块怎么落、坑怎么避、最后怎么验收”的顺序,把这份示例拆成一份能直接参考的方案。
2. 需求与选型:为什么这个场景非 DDD 不可
2.1 先判断业务复杂度,再决定要不要上 DDD
DDD 不是万能药,这一点必须先说清楚。示例代码之所以值得研究,是因为它对应的业务场景通常具备两个特征:一是业务规则多且会变,二是同一套概念在多个流程里反复出现。比如订单、库存、账户这类对象,既有状态流转,又有校验规则,还有跨模块协作。如果只是简单的增删改查,硬套 DDD 只会让代码量翻倍,维护成本反而更高。
我一般会用一个简单判断:把需求里的名词列出来,如果这些名词之间存在明显的生命周期、状态约束和不变量(比如“订单金额必须等于明细之和”“库存不能为负”),那就说明领域模型有存在价值。示例代码正是把这些不变量收进聚合内部,而不是散落在 Service 里。这一点是整份方案的地基,后面所有分层都是为它服务的。
2.2 技术选型:分层架构 + 聚合根 + 仓储抽象
从示例代码的结构看,它采用的是经典的四层划分:接口层、应用层、领域层、基础设施层。这个选型不是拍脑袋,而是为了让依赖方向单向指向领域层。领域层不依赖任何框架和数据库,应用层负责编排用例,接口层只做协议转换,基础设施层实现仓储和技术细节。
| 层次 | 职责 | 依赖方向 | 典型内容 |
|---|---|---|---|
| 接口层 | 接收请求、返回响应 | 依赖应用层 | 控制器、DTO 转换 |
| 应用层 | 编排用例、事务边界 | 依赖领域层 | 应用服务、命令对象 |
| 领域层 | 业务规则、不变量 | 不依赖外层 | 聚合根、实体、值对象、领域服务 |
| 基础设施层 | 持久化、外部集成 | 实现领域层接口 | 仓储实现、ORM 映射 |
这张表是整份方案的核心约束。很多同学翻车就翻在“领域层偷偷 import 了 ORM 注解”,一旦这样,领域模型就和数据库绑死,单元测试也跑不起来。示例代码里领域层是干净的,这一点在改造时必须守住。
2.3 适用边界:什么该放进聚合,什么该放外面
示例代码里最容易被误用的地方,是把所有相关对象都塞进一个聚合。聚合的边界应该由“一致性边界”决定:同一个事务内必须保持一致的才放进去,跨聚合的一致性用领域事件或应用层协调。比如订单和库存,订单创建时扣库存,这两者通常不在一个聚合里,示例里也是通过应用服务编排两个聚合的操作。理解这一点,比记住代码长什么样更重要。
3. 总体结构设计:四层划分与模块职责落地
3.1 目录结构怎么映射到分层
示例代码的目录通常按层组织,再在层内按限界上下文划分模块。我一般会建议读者先画一张模块依赖图,把每个包的责任写清楚,再动手改代码。下面是一个贴近示例风格的目录骨架,语言用 Java 示意,其他语言按同样思路平移:
// 目录结构示意(非完整代码) src/main/java ├── interfaces/ // 接口层:控制器、请求响应对象 │ └── order/ │ ├── OrderController.java │ └── OrderRequest.java ├── application/ // 应用层:用例编排、事务 │ └── order/ │ ├── OrderApplicationService.java │ └── CreateOrderCommand.java ├── domain/ // 领域层:核心业务规则 │ └── order/ │ ├── Order.java // 聚合根 │ ├── OrderItem.java // 实体 │ ├── Money.java // 值对象 │ ├── OrderRepository.java// 仓储接口 │ └── OrderDomainService.java └── infrastructure/ // 基础设施层:技术实现 └── persistence/ └── JpaOrderRepository.java这段结构的关键不在文件名,而在依赖箭头:interfaces依赖application,application依赖domain,infrastructure实现domain里的接口。改造时如果发现某个类跨层引用,基本就是设计出了问题。参数上要注意,仓储接口定义在领域层,实现放在基础设施层,这是依赖倒置的落点,也是示例代码里最值得抄的一处。
3.2 聚合根与值对象的职责划分
聚合根负责维护聚合内的一致性,外部只能通过聚合根访问内部实体。示例代码里,订单聚合根持有订单明细,所有对明细的修改都通过聚合根的方法完成,而不是让外部直接操作明细列表。值对象则用来表达没有身份的概念,比如金额、地址,它们不可变,相等性由属性决定。
我一般会提醒读者:值对象不要图省事用基本类型代替。金额用BigDecimal裸奔,早晚会在精度和币种上踩坑。示例里把金额封装成值对象,构造时校验非负、运算时保持不可变,这个习惯值得保留。改造时如果业务简单,可以先只封装一两个核心值对象,不必一次性全上。
3.3 应用服务与领域服务的分工
应用服务只做编排:加载聚合、调用领域方法、保存聚合、发布事件,不写业务规则。领域服务处理跨聚合或不属于单个实体的规则。示例代码里这两者的边界比较清晰,但初学者常把校验逻辑写进应用服务,导致领域层被架空。
一个可操作的判断标准:如果一段逻辑只依赖聚合内部状态,就放进聚合根;如果依赖多个聚合或外部服务,就放领域服务;如果只是协调步骤和事务,就放应用服务。按这个标准过一遍示例代码,基本能看懂每个类为什么在那个位置。
4. 关键模块实现:从聚合到仓储的可执行细节
4.1 聚合根的实现要点
聚合根要保证不变量在任何入口都被校验。示例代码通常会在构造和状态变更方法里做校验,而不是依赖数据库约束。下面是一段贴近示例风格的聚合根片段:
public class Order { private final OrderId id; private final List<OrderItem> items; private OrderStatus status; public Order(OrderId id) { this.id = id; this.items = new ArrayList<>(); this.status = OrderStatus.CREATED; } // 添加明细时校验:已支付订单不允许修改 public void addItem(ProductId productId, Money price, int quantity) { if (status == OrderStatus.PAID) { throw new IllegalStateException("已支付订单不可修改"); } if (quantity <= 0) { throw new IllegalArgumentException("数量必须为正"); } items.add(new OrderItem(productId, price, quantity)); } public Money totalAmount() { return items.stream() .map(OrderItem::subtotal) .reduce(Money.zero(), Money::add); } }这段代码的重点是:状态校验和数量校验都在聚合内部完成,外部无法绕过。totalAmount由明细实时计算,避免冗余字段不一致。参数上,Money是值对象,OrderId是标识值对象,改造时可以把它们换成自己业务里的对应概念。注意不要在这里引入任何持久化注解,保持领域层纯净。
4.2 仓储接口与实现的分离
仓储在领域层只暴露集合式接口,基础设施层负责实现。示例代码里常见的是接口加实现两段式:
// 领域层:只定义能力,不关心存储 public interface OrderRepository { Order findById(OrderId id); void save(Order order); } // 基础设施层:具体持久化实现 public class JpaOrderRepository implements OrderRepository { private final EntityManager em; public JpaOrderRepository(EntityManager em) { this.em = em; } @Override public Order findById(OrderId id) { return em.find(Order.class, id.value()); } @Override public void save(Order order) { em.merge(order); } }接口放在领域层,实现放在基础设施层,应用服务只依赖接口。这样单元测试可以用内存实现替换,不必起数据库。参数说明:OrderId建议封装成值对象,避免把裸字符串传得到处都是;save的语义是“保存整个聚合”,不要拆成多个细粒度更新方法,否则聚合一致性会被破坏。
4.3 领域事件的发布与消费
跨聚合协作靠领域事件。示例代码里通常在聚合根收集事件,应用服务在保存后发布。实现上要注意两点:事件发布应在事务提交后,避免回滚后事件已发出;消费方要幂等。下面是一个简化的发布片段:
public class OrderApplicationService { private final OrderRepository repository; private final EventPublisher publisher; public void pay(OrderId id) { Order order = repository.findById(id); order.pay(); // 聚合内状态变更并收集事件 repository.save(order); publisher.publish(order.pullEvents()); // 事务提交后发布 } }pullEvents取走事件并清空,防止重复发布。参数上,事件对象应包含聚合标识和必要快照,不要传整个聚合引用。改造时如果项目规模小,可以先用同步发布,但接口要留好,后面换消息队列不用改领域层。
5. 避坑与排查:改造示例代码时最容易翻车的五处
5.1 领域层被框架污染
现象:领域类上出现 ORM 注解、序列化注解,单元测试必须起容器。原因:图省事直接在实体上加持久化映射。解决:领域对象与持久化对象分离,用映射层转换;至少保证领域层不 import 框架包,用架构测试守住这条线。
5.2 聚合过大导致并发冲突
现象:两个用户同时操作同一订单的不同明细,频繁乐观锁失败。原因:把太多不相关的对象塞进一个聚合。解决:按一致性边界拆分聚合,跨聚合用事件最终一致;示例里订单和库存分开就是正确示范。
5.3 应用服务写成业务大杂烩
现象:应用服务里出现大量 if 校验和计算逻辑,领域层变成数据容器。原因:没分清编排和规则的边界。解决:按 3.3 的判断标准把逻辑下沉到聚合或领域服务,应用服务只保留加载、调用、保存、发布四步。
5.4 仓储实现泄漏查询细节
现象:应用层直接拼查询条件,换存储时到处改。原因:仓储接口设计成万能查询器。解决:仓储只暴露聚合级别的操作,复杂查询走独立的读模型;示例里的接口保持精简,改造时不要随意加方法。
5.5 事件重复消费导致数据错乱
现象:同一事件被处理两次,库存被扣两次。原因:发布重试或消费确认丢失,消费方没有幂等。解决:消费方用事件 ID 去重,或设计成幂等操作;发布侧保证事务提交后发送,失败可重试。
6. 验收与答辩:怎么证明这套 DDD 方案真的立住了
验收这套方案,我一般不看代码量,看四件事能不能讲清楚。第一,聚合边界图:每个聚合根、内部实体、不变量列出来,能说明为什么这样切。第二,依赖方向检查:用架构测试或人工走查证明领域层没有反向依赖,这是答辩时最容易被追问的点。第三,用例走查:挑一个核心流程,从接口层到仓储实现完整走一遍,说明每层做了什么、没做什么。第四,测试覆盖:聚合根的不变量有单元测试,应用服务有用例测试,仓储有集成测试。
下面这张验收清单可以直接拿去对照:
| 验收项 | 合格标准 | 检查方式 |
|---|---|---|
| 聚合边界 | 每个聚合有不变量说明 | 设计文档 + 代码走查 |
| 依赖方向 | 领域层无框架依赖 | 架构测试或 import 扫描 |
| 用例完整性 | 核心流程四层贯通 | 端到端演示 |
| 测试覆盖 | 聚合规则有单测 | 测试报告 |
| 事件一致性 | 发布时机与幂等有说明 | 代码评审 |
答辩时有个技巧:主动讲一处你改过的设计决策,比如“示例里这个聚合我拆成了两个,因为一致性边界不同”,比背概念有说服力得多。评委更想听你为什么这么选,而不是 DDD 的定义。
血泪经验是,我早期改造这类示例时,总想一次性把所有模式都用上,结果领域层堆了一堆用不上的领域服务和工厂,答辩时被问“这个类不删会怎样”直接卡住。从那以后我每次改示例代码,都强制先画聚合边界和依赖图,再动一行代码,改完再跑一遍架构检查。希望帮到你。
本文还有配套的精品资源,点击获取