加载中...

本篇要搞懂的 4 件事:怎么把"业务视角的领域对象"一个个翻译成"代码里的包/类/方法"、领域层 6 类对象(实体/聚合根/值对象/领域事件/领域服务/仓储)各自从哪来、现代结构下怎么保证"领域模型 ↔ 代码"长期不跑偏、以及读写分离/对象转换/充血模型这些现代取舍。
⚠️ 本笔记是现代化改写版。原课(约 2019)讲"领域对象 → 代码对象"的映射方法是对的、至今有用;但它的"严格逐层封装""DO/DTO/PO/VO 四件套全转换"在今天偏重。这里保留映射方法的精华,按 13 讲那套模块化单体结构来落,并补上 CQRS、Pydantic 边界转换、import-linter 守一致性等现代做法。原课原图作为对照放在文末。

承接前文:12 讲用事件风暴建出领域模型,13 讲搭好了现代模块化单体的代码骨架(modules/<ctx>/{api,application,domain,infrastructure})。中间还差关键一步——怎么把"业务语言的领域对象"翻译成"代码里的类和方法",并保证它俩长期对得上。14 讲讲的就是这座桥。

12 讲:事件风暴 → 领域模型(聚合/实体/命令/领域事件,业务语言)
13 讲:模块化单体 → 代码骨架(modules/<ctx>/ 四层)
14 讲:把领域对象逐个翻译成代码对象 → 映射表 → 落进模块;并用工具保证长期一致
        ↑ 本讲核心

保证一致性,现代答案是"三道关"

原课靠"画一张映射表 + 人工对齐"。现代在它之上加了两道自动关,三道一起才真正锁住一致性:

关卡 做什么 谁来守
① 映射表(原课精华,保留) 每个领域对象明确对应哪个包/类/方法 人(设计时)
② 充血的 domain/(防架空) 业务规则必须落在 domain/ 的实体/领域服务里,不许漏到 application/api 人 + code review + AI 提示
③ import-linter 合同(自动) domain 不许依赖外层、模块间不许互相 import 内部 机器(CI 每次跑)

💡 一句话:原课保证的是"建模时对得上",现代多保证了"以后改代码也别想偷偷跑偏"——第 ③ 关让 AI 或队友一旦把领域逻辑写错层,CI 立刻红灯。

第一步:把领域对象整理成一张表(这步不变,仍是起点)

微服务/模块边界一定,第一件事不是写代码,而是把事件风暴冒出来的所有领域对象登记成一张表:聚合、实体、命令、领域事件,连同业务行为全列上。四列层层包含:领域模型 → 聚合 → 领域对象 → 领域类型(聚合根/实体/命令/领域事件…)。

领域对象整理表(个人客户领域模型)

看图抓什么:左边"个人客户"领域模型下挂"个人客户""客户归并"两个聚合;每个对象都打了标签——个人客户=聚合根,地址/待归并客户=实体,客户已创建/客户已归并=领域事件,创建个人信息/归并客户等动作=命令。这一步纯粹是盘点登记,把脑子里和白板上的东西落成结构化清单。

🪞 辅助理解:这像装修前先做"房间清单"——先把"客厅、卧室、厨房各放什么"列清楚,再谈布线买家具。清单没列全,后面准返工。这张表在 12 讲也强调过,是建模的正式交付物。

第二步:领域层的 6 类对象怎么来、放哪(映射到现代目录)

事件风暴结束时聚合里通常只有:聚合、实体、命令、领域事件。深一层设计后,会"长出"聚合根、值对象、领域服务、仓储。逐个看它们从哪来、在现代结构里放哪个文件:

# 对象 怎么来的 放哪(现代模块内)
1 实体 一般 1:1 对应数据库表;可拆多表或把属性降级为值对象。充血模型:业务逻辑写在实体自己身上 domain/<entity>.py
2 聚合根 特殊实体,管聚合内其它实体的生命周期;通过工厂+仓储初始化/持久化 domain/<entity>.py
3 值对象 把某属性/属性集设计成不可变值对象(如证件类型用枚举) domain/value_objects.py(frozen dataclass / Enum)
4 领域事件 会触发下一步业务的事件:定范围、事件实体、发布订阅 事件本体 → domain/events.py;订阅处理 → application/event_handlers.py;发件箱 → core/outbox
5 领域服务 一个动作跨多个实体时才设计,把多个实体方法组合起来 domain/services.py
6 仓储 每个聚合一个,含接口 + 实现,靠依赖倒置解耦 接口 → domain/repository.py;实现 → infrastructure/repository_impl.py

实体 vs 值对象怎么选(常纠结):

  • 依附在别的实体里、只能整体替换(不单独改字段)→ 值对象
  • 多条记录、还要基于它查询统计实体

🪞 辅助理解(聚合根像户主):聚合根是聚合这个"小家庭"的户主。地址、电话、银行账号是成员,外人不能直接联系,要找这家人只能先找户主,由它统一对外、统一管成员"生老病死"。仓储是这家的"户口本+保险柜"——把全家信息存进数据库再读出来。

💡 现代关键点:充血,别让 domain 沦为数据袋。 我在你 BIE 的 knowledge/domain/ 看到它是空的、逻辑都在 application——对"解析+CRUD"型模块没问题;但像 memory_kernel、auth 这种有真实业务规则的模块,要警惕 AI 把规则(“记忆冲突怎么判定”“token 怎么失效”)写进 application 甚至 api,domain 只剩 @dataclass判断口诀:删掉这段逻辑会算错业务/违反规则 → 它属于 domain(见 07 讲那把"对错判断 vs 流程编排"的刀)。

第三步:建立"领域对象 ↔ 代码对象"映射表(核心产物)

把每个领域对象彻底翻译到代码,落成一张映射表——它是"业务"和"代码"之间的合同。七列:

含义 现代例子
在模块内哪一层 application / domain
领域对象 业务名 “创建个人客户信息”
领域类型 DDD 类型 聚合根/实体/值对象/领域事件/应用服务/领域服务/仓储
依赖的领域对象 调用/聚合依赖 应用服务"创建客户"依赖领域服务"创建客户"
包名 代码包路径 modules.customer.domain(不再是全局 *.person.domain
类名 代码类 PersonAddressPersonService
方法名 代码方法 create_person_infocreate_address

顺着读就是一条完整链路:聚合根"个人客户"= 类 Person(住 modules/customer/domain/person.py);方法"创建客户信息"= Person.create_person_info;写用例封装成 application/commands/create_person.py;仓储接口 CustomerRepositorydomain/repository.py)+ 实现 CustomerRepositoryImplinfrastructure/repository_impl.py)。

原课这张映射表把七列画得很全(只是包名是旧的全局风格 *.person.domain.*,换算成现代的 modules/<ctx>/... 即可):

领域对象 ↔ 代码对象映射表(个人客户)

🪞 辅助理解:这张表是一本"中英词典"——左半边业务话(创建个人客户信息),右半边代码话(create_person_info)。业务和开发各看各的列,说的是同一个东西。这就是 DDD 反复强调的"通用语言"落到实处。对你这种"会设计、代码靠 AI 写"的人,这张表就是你交给 AI 最好的 spec:你定表,AI 填类体。

现代化的三处取舍(相对原课)

取舍 1:读写分离(CQRS lite)—— 查询别硬走聚合

原课读写都走聚合+仓储。现代普遍写走聚合、读走投影

  • (命令):application/commands/,加载聚合→调聚合方法→仓储保存,保证一致性。
  • (查询):application/queries/直接查只读模型/拼 SQL,不为了查个列表硬组装整个聚合。

💡 为什么?为"查询"去重建完整聚合既慢又啰嗦。读写分开后,写端保持充血严谨,读端怎么快怎么来。

取舍 2:对象转换别学四件套,用 Pydantic 在边界转一次

原课要 DO/DTO/PO/VO 四种对象层层手工转换,样板代码灾难。现代:

  • PO(持久化对象):ORM 类,住 infrastructure/orm.py
  • 领域对象domain/ 的实体/值对象。
  • DTOapi/schemas.py 的 Pydantic 模型,只在 API 进出口转一道,负责收窄字段(别把内部字段裸奔给前端)。

中间能少转就少转,简单模块 PO≈领域对象时不必硬拆。转换交给 MapStruct(Java)/ Pydantic(Python)这类工具,别手写一堆 assembler

取舍 3:依赖方向靠工具守,不靠"逐层封装"的死规矩

原课强调"实体方法→领域服务→应用服务,绝不跨层"的逐层封装。现代不再死抠"每一跳都得封一层"(单实体方法没必要硬包成领域服务),但依赖方向这条底线由 import-linter 焊死:api→application→domain←infrastructure,domain 谁都不依赖。规矩从"人工逐层包"变成"机器查方向"。

非典型领域模型:找不到聚合根怎么办(概念不变)

有些业务是一堆相互独立、主要做分析/计算的实体,高内聚但找不出能当户主的聚合根,又不值得单拆微服务。

例:客户域的"客户归并"——扫描所有客户,按身份证/电话判重再合并。没有天然聚合根。

办法:照样借用聚合思想圈这部分功能,照常建实体属性方法、做服务封装、设计仓储、建依赖关系。唯一区别是没聚合根——除了"聚合根管理"这一项,DDD 其它手法照用。

🪞 辅助理解:典型模型是"有户主的家庭",非典型是"合租室友"——没户主管你们,但还是一个屋檐下、还要分摊水电定公约。组织方式照搬,只少了那个统一对外的户主。

总结

这一讲是从领域模型到代码落地的"最后一公里":把业务对象逐个翻译成包/类/方法,并保证模型与代码长期一致。

三步走:①整理——领域对象登记成"领域模型/聚合/对象/类型"清单;②设计——深一层让聚合长出聚合根/值对象/领域服务/仓储,按表放进现代模块的 domain/application/infrastructure③映射——产出七列映射表(层/对象/类型/依赖/包/类/方法),它等价于代码目录树。

现代相对原课的三处升级:读写分离(查询不走聚合)、Pydantic 在 API 边界转一次(替代四件套手工转换)、依赖方向交给 import-linter(替代死板逐层封装)。再加一条贯穿始终的纪律——让 domain 充血、别被架空,这是 AI 代写时代最该自己盯死的点。

一句话速记

领域对象 → 七列映射表 → 落进 modules/<ctx>/{domain,application,infrastructure};写走聚合、读走投影,DTO 只在 api 边界转一次,依赖方向交给 import-linter 守;核心模块的 domain 必须充血,别让 AI 把业务规则漏到外层。

思考题

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

  1. 先画表、再让 AI 填:挑一个有真实规则的模块(如 memory_kernel),先自己填出"领域对象 ↔ 包/类/方法"七列映射表,再丢给 AI 生成骨架。对比给表 vs 不给表,AI 产出的结构差多少?
  2. 抓"domain 被架空":扫一遍你某个核心模块的 domain/——里面的实体是带行为方法(充血),还是只剩 @dataclass、规则全在 application?挑一两条该回家的规则,列出"它现在在哪、该挪去 domain 的哪个方法"。
  3. 给读写分个家:看看你某模块的查询是不是也走了聚合+仓储。试着把一个"列表/统计"类查询改成 application/queries/ 里直接查只读模型,体会 CQRS lite 的轻快。

附:原课 2019 版怎么对应

原课用"个人客户"为例,方法论和本笔记完全一致(整理表 → 深设计 → 映射表),差别只在两点:

  • 结构:原课是"全局四层 + 一上下文一微服务",现代是 modules/<ctx>/ 模块内分层(见 13 讲)。映射表里的包名 *.person.domain.* 换算成 modules/customer/domain/* 即可。
  • 手法:原课强调"实体方法→领域服务→应用服务"的严格逐层封装DO/DTO/PO/VO 四件套转换;现代弱化了这两点(见上文「三处取舍」)——依赖方向交给 import-linter,DTO 只在 api 边界用 Pydantic 转一次。

💡 原课有一条仍值得抄进规范:命名用后缀区分层次——*DomainService*AppService,看名字就知在哪层;而"多个应用服务总在重复编排同几个领域服务"= 该合并它们的信号。这两条放到现代依然成立。

(原课"整理表""映射表"两张图本笔记已挪到正文对应步骤;“服务逐层封装图”"扁平目录树图"因与现代做法不符已不在本笔记展示,需要可看 原文。)

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