
简介这份资源是领域驱动设计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 示例代码都强制先跑一遍聚合不变量测试再谈其他因为边界对不对测试比目录结构诚实得多。希望帮到你。本文还有配套的精品资源点击获取