简介:这份资源是领域驱动设计(DDD)的官方示例代码,面向希望将 DDD 方法论落地到实际项目的中高级 Java 开发者与架构学习者,以经典船运业务为背景,帮助理解如何用领域模型组织复杂业务逻辑。压缩包共 426 个文件,约 23.57MB,以 142 个 Java 源码和 110 个编译类为核心,辅以 56 个 HTML 页面、33 个 XML 配置、35 个依赖 jar 及少量 JSP、properties、SQL 与图片资源,构成一个可运行的完整工程。代码围绕船只、货物、航线、港口、船期等实体与值对象展开,覆盖聚合、领域事件、领域服务、限界上下文、仓储持久化等关键概念,并配有示例数据生成与货物跟踪、订舱管理等模块,便于对照源码理解各层职责划分与模型边界。已有 1901 人学习,适合作为研读 DDD 分层架构、复盘建模思路的参考样例。
1. 拿到一份 DDD 官方示例代码,先别急着跑
很多人第一次接触领域驱动设计,都是从一份官方示例代码开始的。下载下来,解压,看到一堆分层目录和接口,心里想的是“这不就是个普通的分层项目吗”。结果打开一个聚合根,发现里面塞了十几个方法,仓储接口和实现类各写一遍,领域事件发出去之后没人监听,最后跑起来能编译通过,但业务逻辑根本串不起来。这份领域驱动设计官方示例代码的价值不在于“能跑”,而在于它把 DDD 的战术模式——聚合、实体、值对象、领域服务、仓储、领域事件——用可编译的代码摆在你面前,让你看到边界在哪里、职责怎么分。它适合已经写过 CRUD、被贫血模型坑过、想搞清楚“业务逻辑到底该放哪”的后端开发者。下面我按自己拆这份代码的顺序,从目录结构到聚合设计,再到事件流转和踩坑,一步步说清楚怎么用。
2. 拆目录与分层:先看清代码把什么放在了哪
2.1 四层结构在示例里的真实落点
拿到代码先别打开 IDE 的全局搜索,先看目录。这份示例通常按接口层、应用层、领域层、基础设施层来切,但真实项目里最容易出问题的是“领域层里混进了基础设施的依赖”。我一般会先做一件事:把领域层目录单独拎出来,检查它的 import 里有没有出现数据库驱动、HTTP 客户端、消息队列的包。示例代码里领域层应该是纯的,只依赖语言标准库和领域内自己的类型。
# 先看领域层的依赖情况,重点找基础设施相关的 import grep -rn "import" src/Domain/ | grep -iE "sql|http|redis|kafka|orm" || echo "领域层干净"这段命令的逻辑很简单:领域层如果出现了sql、http、redis这类关键词,说明分层已经被穿透了。参数上,src/Domain/要换成你实际解压后的领域层路径,不同语言的示例目录名可能是domain、core或model。跑出来有输出不代表代码错,但你要知道这是示例为了简化做的妥协,自己落地时不能照抄。
2.2 聚合根和实体的区分标准
示例里最值得看的是聚合根怎么定义。很多新手会把所有有 ID 的对象都叫实体,把所有实体都当聚合根,结果一个订单下面挂了几十个可独立查询的对象,事务边界彻底失控。这份代码里聚合根通常有几个特征:有全局唯一标识、是外部访问的唯一入口、内部实体只能通过聚合根拿。我一般会打开聚合根类,数一下它对外暴露的方法,如果超过十个,就要警惕是不是把应用层的编排逻辑塞进来了。
# 示例中聚合根的典型结构(伪代码,按实际语言对照) class Order: # 聚合根 def __init__(self, order_id, items): self.id = order_id self.items = items # 内部实体列表,不对外暴露可变引用 self.status = "created" def add_item(self, item): # 业务方法,内部校验不变量 if self.status != "created": raise DomainError("已确认订单不能加项") self.items.append(item) def confirm(self): # 状态流转,返回领域事件 if not self.items: raise DomainError("空订单不能确认") self.status = "confirmed" return OrderConfirmed(self.id)这段代码的关键不在语法,而在两个约束:items不提供公开的 setter,外部只能通过add_item改;confirm里做了不变量校验并返回事件。参数说明上,order_id建议用值对象包一层而不是裸字符串,示例里如果用了裸类型,你自己落地时要补上。逻辑说明:聚合根的方法应该表达业务动作,而不是 getter/setter 的堆砌,这是判断一份示例代码质量的第一眼标准。
2.3 仓储接口放领域层、实现放基础设施层
示例里仓储接口和实现是分开的,接口在领域层,实现在基础设施层。这个安排不是形式主义,它决定了领域层能不能被单独测试。我见过不少项目把仓储接口和 ORM 实现写在一个文件里,结果领域层单元测试必须连数据库。检查方法很直接:看领域层的仓储接口有没有引用任何持久化框架的类型。
# 确认仓储接口是否纯净:接口文件里不应出现 ORM 或驱动类型 grep -rn "Repository" src/Domain/ | head -20跑完看输出,如果接口方法的参数和返回值都是领域类型,说明分层是对的;如果出现了Session、Connection、QuerySet这类词,说明示例在这一层做了简化,你落地时要自己抽干净。这一步的产出是你对“哪些代码可以脱离框架跑”的判断,后面写测试会直接受益。
3. 把聚合跑起来:从用例到事务边界的完整链路
3.1 应用服务只做编排,不写业务规则
示例里的应用服务通常很薄,负责加载聚合、调用领域方法、保存聚合、发布事件。很多人翻车的地方是把校验逻辑写进应用服务,导致同一规则在多个用例里重复。我一般会拿一个用例对照看:应用服务里有没有if判断业务状态,如果有,就该下沉到聚合根。
# 应用服务:只编排,不判断业务规则 class ConfirmOrderService: def __init__(self, order_repo, event_publisher): self.order_repo = order_repo self.event_publisher = event_publisher def execute(self, order_id): order = self.order_repo.find_by_id(order_id) # 加载聚合 event = order.confirm() # 业务规则在聚合内 self.order_repo.save(order) # 保存聚合 self.event_publisher.publish(event) # 发布事件逻辑说明:execute里没有任何业务判断,confirm抛出的领域异常由上层统一处理。参数上,order_repo是接口注入,示例里可能用构造函数注入,你落地时换成依赖注入容器即可。这段代码的价值是给你一个“薄应用层”的模板,照着改比从零设计快得多。
3.2 一个聚合一次事务,别跨聚合改数据
示例代码里的事务边界通常围绕单个聚合。我拆的时候会专门找有没有一个用例同时修改两个聚合的情况,如果有,要么是示例为了演示简化了,要么是聚合划分有问题。常见做法是:跨聚合的一致性用领域事件做最终一致,而不是一个大事务包住。
# 错误示范:一个事务里改两个聚合 def transfer(self, from_id, to_id, amount): a = self.repo_a.find(from_id) b = self.repo_b.find(to_id) a.deduct(amount) b.add(amount) self.repo_a.save(a) self.repo_b.save(b) # 两个聚合同事务,锁竞争和失败回滚都难处理这段不是让你照抄,而是让你在示例里找类似结构并标记出来。参数说明:amount这类跨聚合操作,合格的做法是拆成两个用例,中间用事件驱动,或者把两者合并成一个聚合——但合并前要确认它们真的有一致性边界。这一步的产出是一张“哪些用例碰了多个聚合”的清单,后面重构时按清单逐个处理。
3.3 领域事件从发布到消费的闭环
示例里领域事件的发布往往只写了一半,发出去之后没有消费者,跑起来看不到效果。我一般会补一个最小的内存事件总线,把闭环跑通再谈持久化。
# 最小事件总线:先跑通闭环,再换消息队列 class InMemoryEventBus: def __init__(self): self.handlers = {} def subscribe(self, event_type, handler): self.handlers.setdefault(event_type, []).append(handler) def publish(self, event): for handler in self.handlers.get(type(event), []): handler(event) # 同步调用,便于调试;生产环境换异步逻辑说明:subscribe按事件类型注册处理器,publish同步分发,方便你在单步调试时看到事件流向。参数上,生产环境要把同步换成异步并加幂等,示例里通常不体现这些。跑通这个闭环之后,你再去对照示例里的事件定义,就能判断哪些事件该带聚合 ID、哪些该带快照数据。
4. 避坑与排查:示例代码照抄必翻车的几个点
4.1 现象:聚合根方法越写越多,最后变成上帝类
原因:示例为了演示方便,把多个用例需要的操作都堆在一个聚合根上,你照抄后业务一复杂就失控。解决:按“一次业务动作一个方法”拆,方法内部只校验本聚合的不变量,跨聚合编排放应用服务。我一般会设一个阈值,聚合根公开方法超过八个就回头检查职责。
4.2 现象:值对象用了可变类型,改一处全乱
原因:示例里有些值对象直接用列表或字典承载,没有做不可变封装。解决:值对象构造后不可变,需要修改时返回新实例。检查方法是看值对象有没有暴露修改内部状态的方法,有就改掉。
4.3 现象:领域事件发了但消费端拿不到数据
原因:事件里只带了 ID,消费端反查时聚合已经变了。解决:事件里带够消费端需要的快照字段,或者约定消费端在事件发布后短时间内查询。示例里常见只带 ID 的写法,落地时要按消费场景补字段。
4.4 现象:仓储实现里写了业务查询,领域层被绕过
原因:示例的仓储接口只有find_by_id和save,实际开发中为了省事直接在实现类里加复杂查询,业务逻辑漏到基础设施层。解决:复杂查询单独建读模型或查询服务,仓储接口保持窄。这条是我踩过最深的坑,后面每次拆示例都会先看仓储接口的方法数。
5. 进阶验证:用测试反推聚合边界是否合理
拆完示例、跑通链路之后,我习惯用测试来验证聚合边界。具体做法是给每个聚合根写一组不变量测试,不连数据库,纯内存跑。如果某个测试必须 mock 掉大量外部依赖才能写,说明这个聚合的边界划错了,或者业务逻辑漏到了外面。
# 聚合不变量测试:不连库,纯内存验证 def test_confirm_empty_order_raises(): order = Order("o1", []) with pytest.raises(DomainError): order.confirm() def test_add_item_after_confirm_raises(): order = Order("o1", [Item("i1")]) order.confirm() with pytest.raises(DomainError): order.add_item(Item("i2"))这两条测试对应两个不变量:空订单不能确认、已确认订单不能加项。参数上,Order和Item用示例里的真实类型替换,DomainError换成示例的异常基类。逻辑说明:测试写得越轻,越能说明聚合自包含;如果测试里出现mock_repo、mock_session,就要回头查聚合是不是依赖了外部状态。
再进一步,我会把示例里的领域事件消费链路也纳入验证:发布一个事件,断言处理器被调用且入参字段完整。这一步能暴露事件定义缺字段的问题,比等到联调时才发现要省事得多。
| 验证项 | 通过标准 | 不通过时的动作 |
|---|---|---|
| 聚合不变量测试 | 纯内存可跑,无外部 mock | 检查业务逻辑是否外泄 |
| 仓储接口纯净度 | 接口无持久化框架类型 | 抽接口或换实现 |
| 事件字段完整性 | 消费端所需字段齐全 | 补快照字段 |
| 应用服务厚度 | 无业务状态判断 | 下沉到聚合根 |
这张表我一般贴在拆解笔记第一页,每拆一个模块勾一项。从那以后我每次拿到新的 DDD 示例代码,都强制先跑一遍聚合不变量测试再谈其他,因为边界对不对,测试比目录结构诚实得多。希望帮到你。
本文还有配套的精品资源,点击获取