加载中...

本篇要搞懂的 4 件事:现代 DDD 代码模型为什么"先按模块切、再在模块里分层"、一个完整的模块化单体长什么样(含 core/shared、模块内四层、架构约束)、它和原课 2019 版的差异在哪、以及怎么照着它让 AI 生成结构干净的代码。
⚠️ 本笔记是现代化改写版。原课(欧创新,约 2019)讲的是"顶层按技术层切 + 一个限界上下文从第一天就拆成微服务"的模型,在今天看偏旧。这里按 2020s 主流的"模块化单体(modular monolith)+ 端口适配" 来讲,并全程用你自己的 MemoryEngine / BIE 当真实例子。原课那套结构作为对照放在文末「附:原课 2019 版」,照样保留可查。

承接前文:12 讲用事件风暴建出了领域模型,13 讲要回答"这套模型落到硬盘上、代码目录到底长什么样"。原课的答案是"照着 07 讲四层架构一比一建四个一级目录"。现代答案在它之上更进一步:顶层不再按技术层切,而是按"限界上下文(模块)"切,四层放进每个模块内部。 为什么这么改、改完长什么样,是本讲重点。

07 讲:DDD 四层架构(接口层/应用层/领域层/基础层)—— 思想地基,至今不变
   ↓
原课 13 讲(2019):顶层 = 四个技术层目录,一个上下文 = 一个微服务(一开始就拆)
   ↓ 演进
现代 13 讲(本笔记):顶层 = 业务模块,四层放进模块内部;先做模块化单体,要了再拆微服务

🪞 辅助理解(两种摆法的区别):把代码想成一栋写字楼。

  • 原课摆法 = 按工种分楼层:1 楼全是"前台接待"、2 楼全是"行政"、3 楼全是"研发"……要办成一件事(一个业务功能),你得在四层楼之间来回跑。
  • 现代摆法 = 按部门分房间:每个部门(限界上下文)有自己独立的套间,套间内部再分前台/办公/后勤。一件事在一个套间里就办完了。哪天某部门要独立出去成立子公司(拆微服务),整套间搬走即可。

现代代码模型:完整参考结构

这是一套可以直接照抄的"模块化单体"骨架(语言无关,用 Python 风格示意,你的 MemoryEngine/BIE 就是这个形状):

project-root/
├── apps/                       # 启动入口(可执行单元),本身几乎不含业务
│   ├── api/                    #   HTTP 服务入口(挂路由、中间件、装配依赖)
│   ├── admin_api/              #   后台管理入口
│   └── worker/                 #   异步任务 / 消息消费者入口
│
├── core/  (或 shared/)         # 跨模块共享内核:所有模块可依赖它,但它不许反向依赖任何业务模块
│   ├── config/                 #   配置
│   ├── database/               #   DB 连接 / session / 基类
│   ├── events/  +  outbox/      #   领域事件总线 + 发件箱(保证"业务+事件"原子落库,见 06 讲)
│   ├── observability/          #   日志 / 指标 / 追踪
│   └── runtime/                #   依赖注入、生命周期
│
├── modules/                    # ★核心★ 顶层按"限界上下文"切,一个上下文一个文件夹
│   ├── <bounded_context>/      #   如 memory_kernel / identity_access / knowledge
│   │   ├── domain/             #     领域层:纯业务,零框架依赖
│   │   │   ├── <entity>.py     #       实体 / 聚合根(充血:带行为方法,不是纯数据袋)
│   │   │   ├── value_objects.py#       值对象(不可变,frozen dataclass)
│   │   │   ├── events.py       #       领域事件("XxxHappened" 过去式命名)
│   │   │   ├── repository.py   #       仓储【接口】(Protocol/ABC) —— 只声明、不实现
│   │   │   └── services.py     #       领域服务(跨多个实体的业务逻辑)
│   │   ├── application/        #     应用层:用例编排,薄
│   │   │   ├── commands/       #       写用例:每个 handler 一个文件(走聚合 + 仓储)
│   │   │   ├── queries/        #       读用例:直接查只读模型/投影,不经聚合(CQRS lite)
│   │   │   └── event_handlers.py#      订阅其它模块发来的领域事件
│   │   ├── infrastructure/     #     基础层:仓储【实现】+ ORM + 外部客户端
│   │   │   ├── orm.py          #       持久化对象 PO(SQLAlchemy 表映射)
│   │   │   ├── repository_impl.py#     实现 domain 里声明的仓储接口(依赖倒置)
│   │   │   └── clients.py      #       调外部服务/第三方 SDK 的适配器
│   │   └── api/                #     接口层:本上下文对外暴露
│   │       ├── routes.py       #       路由 / Controller
│   │       └── schemas.py      #       DTO(Pydantic,进出参,收窄字段)
│   └── <another_context>/      #   结构完全一致
│
└── tests/
    └── architecture/           # 架构测试:import-linter 合同,自动守边界(见下)

💡 注意:四层(接口/应用/领域/基础)一个没少,只是从"顶层四个大目录"挪进了"每个模块内部"。07 讲的分层思想完整保留,变的只是"先分模块还是先分层"的顺序。

这四层就是 07 讲那张图,只不过现在它住在每个模块里:

DDD 四层架构(现代把这四层放进每个模块内部)

三个关键升级(相对原课)

升级 1:模块优先,不是技术层优先

原课顶层是 interfaces/ application/ domain/ infrastructure/ 四个大目录,一个业务功能的代码摊在四处。现代顶层是 modules/<上下文>/,同一上下文的四层收在一个文件夹里——改一个功能,代码集中在一个模块内,不用四处跳。这是 Spring Modulith、各类 vertical-slice 架构的共同主张。

💡 模块里的 domain/ 该放哪些零件?原课这张"聚合内部"图画得最清楚——实体、事件、仓储、领域服务(现代把它们摊平成 domain/ 下的几个文件,思想一样):

聚合内部该有的零件:entity / event / repository / service

升级 2:模块化单体,不是"上来就拆微服务"

原课默认"一个限界上下文 = 一个微服务,第一天就拆"。这是 2019 微服务狂热期的心态,对中小项目是过早优化——分布式带来的跨服务调用、数据一致性、运维成本,往往把团队拖垮。

现代默认:先做模块化单体——逻辑上分模块、边界清晰,但物理上是一个可部署应用。等到某个模块真的需要独立伸缩/独立发布,再把它整体抽出去成微服务(见文末「演进」)。

🪞 辅助理解:模块化单体 = “一栋楼里的多个独立部门”;微服务 = “把部门搬到不同城市开分公司”。先把部门墙砌好(模块边界清晰),搬不搬城市以后再说——而墙砌好了,搬起来才不痛。

升级 3:架构边界靠工具自动守,不靠自觉

原课只能"告诫你别跨层调用"。现代用 import-linter(Python)/ ArchUnit(Java)/ Tach 这类架构测试,把规则写成合同,违规直接 CI 报错。你的 MemoryEngine 和 BIE 都已经这么做了,合同长这样:

[[tool.importlinter.contracts]]
name = "domain 层禁止依赖外层"
type = "forbidden"
source_modules = ["...modules.*.domain"]
forbidden_modules = ["...modules.*.api", "...modules.*.application", "...modules.*.infrastructure"]

[[tool.importlinter.contracts]]
name = "core 不依赖业务模块"
source_modules = ["...core"]
forbidden_modules = ["...modules"]

💡 这正是原课"想要、但当年没有"的东西——它把 07 讲"严格分层"从"君子协定"变成了"机器执法"。AI 帮你写代码时尤其值钱:AI 一旦把 domain 写得依赖了 infrastructure,CI 立刻红灯,不会等到你 review 才发现。

模块之间怎么通信(最容易写错的地方)

铁律:一个模块绝不直接 import 另一个模块的 domain/ 或内部实现。 只能走两条合法通道:

通道 怎么用 适用
领域事件(首选) A 模块发 XxxHappened 事件 → B 模块 event_handlers.py 订阅处理 异步、解耦、最终一致(见 06 讲)
公开应用服务 / 端口 调用对方 application 暴露的接口(或定义一个 Port 接口让对方实现) 需要同步拿结果时

💡 BIE 甚至专门写了合同 "graph 节点禁止反向写业务表""shared.jobs 禁止依赖任何 modules" 来堵这类越界——这就是把"模块间松耦合"焊死。

对照你自己的项目

你的两个项目就是这套模型的活样本,可直接对号入座:

现代模型 MemoryEngine BIE
启动入口 apps/ apps/{api, admin_api, worker} app/(FastAPI 入口)/ studio
共享内核 core/{config,database,events,observability,runtime} app/core + app/shared/{jobs,observability,outbox}
限界上下文 modules/ 9 个:memory_kernel、identity_access、retrieval_composer… 15 个:knowledge、chat、agent、auth…
模块内四层 <模块>/{api, application, domain, infrastructure} 同左
仓储 + DIP infrastructure 里 repository_impl.py + ORM 同左(infrastructure/repository_impl.pyorm.py
事件最终一致 core/events shared/outbox(发件箱模式)
边界自动校验 import-linter 合同 import-linter 合同(更细)

💡 一句话:你已经站在 13 讲想要到达的地方了。剩下要盯的不是结构,而是模块内部 domain/ 会不会被 AI 写成"贫血空壳"(这是 14 讲的主题)。

演进:模块 → 微服务(原课承诺的"后手",现代兑现得更好)

模块边界清晰带来的最大红利就是可演进

  1. 某模块被高频访问、需要独立伸缩 → 把整个 modules/<ctx>/ 文件夹整体抽出成独立服务。因为它本就自包含(自带四层 + 仓储 + 事件),抽离时不用东拼西凑。
  2. 模块间原本就只通过事件/接口通信,抽出后把"进程内事件"换成"消息中间件"即可,业务代码几乎不动。

这正是原课"以聚合为单位拆/合微服务"想要的效果,但现代把粒度提到了"模块"层级、并用工具保证了边界,演进起来更稳。

总结

现代 DDD 代码模型 = 模块化单体:顶层按限界上下文切成 modules/<ctx>/,每个模块内部再放 07 讲那四层(api/application/domain/infrastructure),加一个所有模块共享、但不反向依赖业务的 core/shared

相对原课(2019)的三处升级:①模块优先(功能内聚,不在四个技术层间跳)、②模块化单体优先(先单体后微服务,避免过早分布式)、③用 import-linter 自动守边界(把严格分层从自觉变成执法)。模块之间只通过领域事件公开接口通信,绝不互相 import 内部。需要时把整个模块文件夹抽出去就是微服务。

地基没变——07 讲的分层、依赖倒置、聚合、仓储全在;变的是摆放方式更内聚、演进路径更稳、边界有工具兜底。

一句话速记

现代 DDD = 模块化单体:先按限界上下文切模块、四层放进模块里,core 共享但不依赖业务,模块间只靠事件/接口通信,import-linter 自动焊死边界;先单体后微服务,要拆时整模块搬走。

思考题

你正在做 MemoryEngine / BIE,正好拿真实代码练:

  1. 画一条依赖箭头图:挑一个模块(如 memory_kernel),把它 api → application → domain ← infrastructure 的依赖方向画出来。domain 是不是真的谁都不依赖?infrastructure 是不是只通过实现 domain 的仓储接口来"反向插入"?
  2. 检查模块间通信:你的模块之间有没有出现"A 模块直接 import 了 B 模块 domain 里的类"?如果有,它该改成发领域事件还是调对方应用服务?(可以让 AI 顺着 import-linter 合同扫一遍)
  3. 写一版"目录约束 prompt":把本讲这套模块化单体目录树,连同"业务逻辑放 domain、编排放 application、跨模块只发事件"的规则,整理成一段给 AI 的系统提示。下次让 AI 加新模块时直接喂给它,对比有没有这段提示时生成结构的差距。

附:原课 2019 版怎么对应(一句话换算)

原课的目录名其实和现代一一对应,只是"摆放层级"不同——对照着看就懂:

原课(2019) 现代(本笔记)
顶层 interfaces/ 模块内 api/
顶层 application/infrastructure/ 模块内同名层(名字没变)
顶层 domain/ 里的"聚合包"(entity/event/repository/service) 模块内 domain/ 的几个文件(摊平了)
一个限界上下文 = 一个微服务 一个限界上下文 = modules/<ctx>/ 一个模块(先单体)

💡 最大差别就一处:原课把四层放在"全局四个大目录"下、且默认一上来就拆微服务;现代把四层收进"每个 modules/<ctx>/ 模块"里、默认先单体。思想一脉相承,理解了现代版,原课那套自然秒懂。(原课的 7 张目录截图在 原文 里可查,本笔记只保留了仍适用的"四层架构图"和"聚合零件图"。)

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关