news 2026/10/28 23:03:13

领域驱动设计示例代码拆解:四层架构与聚合实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
领域驱动设计示例代码拆解:四层架构与聚合实现指南

简介:这份资源是领域驱动设计(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 的定义。

血泪经验是,我早期改造这类示例时,总想一次性把所有模式都用上,结果领域层堆了一堆用不上的领域服务和工厂,答辩时被问“这个类不删会怎样”直接卡住。从那以后我每次改示例代码,都强制先画聚合边界和依赖图,再动一行代码,改完再跑一遍架构检查。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 23:03:05

加密视频播放器的反录屏技术解析

1. 加密视频播放器的反录屏机制解析当你在某教育平台观看加密课程视频时&#xff0c;可能会遇到这样的场景&#xff1a;用手机对着屏幕录像时&#xff0c;播放器突然弹出警告或直接终止播放。这种看似"智能"的检测背后&#xff0c;其实是一套融合了多种技术的防御体系…

作者头像 李华
网站建设 2026/10/28 23:01:55

CTF Web安全:Base64编码与HTTP头注入攻防实战

1. CTF Web安全攻防实战解析在CTF竞赛的Web安全赛题中&#xff0c;Base64编码隐藏、HTTP头注入和多层嵌套解码是三类经典考点。这些技术点不仅考验选手的代码审计能力&#xff0c;更要求对Web协议和数据处理流程有深入理解。去年某次线下赛中&#xff0c;一道结合这三项技术的题…

作者头像 李华
网站建设 2026/10/28 22:59:49

长上下文 U 型编排:让证据顺序成为可验证的假设

把最相关的证据放在开头、次相关的放在结尾&#xff0c;可能改善某些长上下文任务&#xff0c;也可能打乱时间顺序、因果链或文档中的例外条件。U 型编排应当是一条可以失败的实验假设&#xff0c;而不是“破解中部遗忘”的通用按钮。 《Lost in the Middle》提供了位置敏感性…

作者头像 李华
网站建设 2026/10/28 22:58:31

VOSviewer 1.6.20安装与使用全指南

1. VOSviewer简介与核心功能解析VOSviewer是由荷兰莱顿大学科技研究中心&#xff08;CWTS&#xff09;开发的文献计量分析工具&#xff0c;专门用于构建和可视化文献网络。这个1.6.20版本在文献计量学领域被广泛使用&#xff0c;主要因为它能处理大规模文献数据并生成直观的知识…

作者头像 李华
网站建设 2026/10/28 22:57:40

FPGA驱动双目OV5640并通过HDMI显示:DVP接口与TMDS编码实战

简介&#xff1a;一套基于FPGA XC6SLX16的双目OV5640图像采集与HDMI显示完整实现方案&#xff0c;面向FPGA初学者与嵌入式视觉开发者&#xff0c;重点解决双目摄像头数据同步、像素读取、时序控制、TMDS编码与实时显示等核心问题&#xff0c;覆盖FPGA基础、Verilog HDL编程、OV…

作者头像 李华
网站建设 2026/10/28 22:56:20

Unity接入华为HMS SDK实战:绕过签名混淆权限ABI四大坑

简介&#xff1a;本资源是面向Unity游戏开发者的华为HMS SDK接入实战Demo&#xff0c;专为需要在华为生态设备上集成账号、推送、游戏服务等功能的中高级开发者设计。资源完整呈现从SDK导入、IL2CPP配置、初始化、登录到各模块API调用的全流程实践&#xff0c;覆盖Android平台核…

作者头像 李华