# 读书记录《悟道领域驱动设计》

LLMS 索引： [llms.txt](/llms.txt)

---

- ch2 应用架构
	- 贫血模型 vs 充血模型
		- 贫血模型：类只是数据容器，没有行为（例如 pojo）
		- 充血模型：类有属性（数据），也有方法（行为）
	- 项目结构
		- 接口层：对外暴露api/消息consumer
		- 应用服务层：协调领域模型完成业务逻辑
		- 基础设施层：写db/缓存/调外部服务
		- 领域层：领域内的具体逻辑
	- 查询的两种实现
		- 1 读数据后加载领域模型（聚合根），聚合根转换为查询结果 view
		- 2 直接用实际存储的数据模型，跳过领域对象加载，直接用把数据模型转换为查询结果 view
- ch3 实体和值对象
	- 实体：有唯一标识的领域模型
		- e.g. 内容发布系统的文章
	- 值对象：没有唯一标识，需要的时候随时构造，值一样就认为是相同的对象
		- e.g. 内容发布系统的文章标题
		- DP：Domain Primitive 领域内的基本数据类型，值对象的一种
			- e.g. 金融系统中的金额（money）
- ch4 聚合与聚合根
	- 聚合：一组对象组成的对象树
		- 单个实体也是聚合
		- 聚合是一致性的边界；聚合内强一致，跨聚合最终一致
			- 一个事务只更新单个聚合
	- 聚合根：这个对象树的入口
		- 单个实体也可以是聚合根
		- 外部对象只能引用聚合根（通常通过持有一个id）
	- 拆分聚合
		- 一个实体被多个聚合根引用，应该被提升为聚合根
		- 1:N 的集合属性（Role-Resource Binding），因为存在双向查找的需要（Role找Resource，Resource找Role），通常也可能被提升为聚合根
- ch5 Factory, Repo, 领域服务
	- Factory：从无到有创建领域对象
		- 需要分配 id 
		- 通常用 builder 模式
	- Repo：保存和加载聚合根，和实际的存储解耦
		- save：聚合根持久化到数据库
			- 注意事务控制，所有实际的写操作应该在一个事务内执行（例如文章写一个表，文章内容写另一个表）
		- load：从数据库查询数据对象，组装回聚合根
		- 推荐实现行级 repo，一个聚合根对应数据库中一行数据
			- 如果存在查多行的需求，如 queryList，应该用 CQRS 分离出去
	- 领域服务：包含领域中不适合放在实体/值对象的业务操作，应该是无状态的
		- 例子：导出数据为 excel
- ch6 设计模式
	- 责任链：多个handler逐个执行
	- 策略：从多种算法实现中选择一个（根据业务类型匹配到策略）
	- 桥接
	- 规约：验证复杂规则
- ch7 防腐层 ACL
	- 避免外部系统变更影响到当前系统（字段名变化、包名变化...）
	- 实现：适配器模式
		- 出入参数应该是本地值对象，或者基本数据类型
		- 外部异常/错误码应该转换为本地异常/错误码
		- 只返回实际需要的字段
		- 一般在应用服务/领域服务调用防腐层，不要在实体和值对象使用
- ch8 领域事件
	- 幂等实现：数据库唯一索引 /  状态机
	- 事件建模：用领域语言（例如账户被激活）；建模为值对象/贫血对象（不可变的）
	- 应用
		- 触发其他领域/聚合的行为
		- 记录状态变化
	- 事件消息体：实体id，事件id，事件类型，发生事件
		- 也可选包含具体数据，减少查询需要，例如用户变更手机号事件带上新的手机号
	- 生成事件
		- 应用层创建：推荐，直接在应用层生成，然后调用基础服务发布
		- 聚合根创建：聚合根内生成事件，存储在聚合根内一个临时位置，应用层调用方法从聚合根取得，然后调用基础服务进行发布
	- 发布事件
		- 问题：保存聚合根和发布领域事件应该是一个事务，不能一个失败一个成功；但是引入分布式事务会造成复杂度上升
		- 解决：用一个db里事件表，repo save的时候不仅写聚合根，还写事件表；然后用事件表变更触发mq
			- 轮询补偿：一个外部定时任务，检查事件表中状态=未发布的事件，读出来发mq，然后更新状态为已发布
			- 拖尾：用一个db拖尾组件监听db变更，自动发布到mq
	- 订阅事件：事件 consumer 作为一个新的接口层，调用应用层服务
		- 注意幂等
- ch9 CQRS
	- 问题：查询的时候加载聚合根可能没必要（例如只读取一部分字段）；但是修改的时候必须要加载完整聚合根
	- CQRS 应用层分为 查询 query 和 修改 command 两部分，分别用不同的模型处理
		- command 依然加载完整聚合根
		- query 直接用数据模型（db数据）进行查询，转换为需要的返回 view
	- 实现
		- 同数据源：比较简单，query 不用 repo.load 而是直接读db
		- 异数据源：复杂，可能导致不一致
			- 需求：主数据存储 mysql，用 es 做文本搜索
			- 实现：应用层发布领域事件，db日志拖尾
			- 查询可以是一个贫血模型，因为没有复杂逻辑且只读数据不修改
	- 缺点：复杂度高、可能导致数据一致性问题、增加学习成本
- ch10 事件溯源
	- 想法：存储领域事件，读取时通过重放领域事件得到最新聚合状态
		- 优点：完整业务跟踪能力；可以回滚到任意时刻的聚合根
	- 实现1：最原始的实现，直接存领域事件，读时回放
		- 只有一个事件表
	- 实现2：事件多查起来慢，因此加快照，回放时用 上次快照 + 上次快照后的事件
		- 事件表 + 快照表，save的时候可能需要生成快照
	- 实现3：快照表用拉链表实现，存储所有事件+聚合根所有版本（含有效期）
		- 拉链表：含有开始时间和结束时间，表明这一行数据在此时间段内有效
- ch11 一致性
	- 聚合内一致性
		- 事务应该在 repo 实现，不应该在应用层实现，否则会造成事务过大
		- 用乐观锁避免并发更新问题
		- repo save 失败，需要在应用层重试，来确保聚合数据是最新状态（即重新 load）
			- 因为会重新调用外部接口，依赖的外部接口应该幂等
			- 不适合频繁更新的热点数据（可能导致频繁重试）
			- 重试次数规划好，一般一次就够了，多次的话应该考虑其他架构
			- 重试次数/触发原因可配置，一般只有乐观锁失败再重试，其他错误不应该重试
		- 读写性能问题
			- 一般业务做不到那么大
			- 读多写少：读写分离、缓存、复杂查询维护单独的读数据源、分库分表
			- 写之前的确需要完整加载聚合，写慢点就忍吧
	- 跨聚合一致性：实际上是分布式事务
		- 二阶段提交：prepare, commit；commit 可能失败，此时rollback
			- 代价比较大
		- 本地消息表+发布领域事件：见 ch8
		- 最大努力通知：上游不断发起通知调用给下游，直到下游确认
			- 适用于对可靠性要求不高
			- 发起者需要提供查单接口，供接收者主动查询状态
			- 发起者需要实现重复通知，接收者自己保证幂等
			- 发通知间隔应该指数退避，且限制最大次数，避免无限发送
		- TCC：try, confirm, cancel；try成功了confirm必须成功
			- 注意点1 幂等：confirm/cancel幂等
			- 注意点2 空回滚：没有调用try就调用了cancel；收到cancel查资源状态，没try过应该直接返回
			- 注意点3 事务悬挂：先执行cancel再执行try；收到try查资源状态，被cancel过也直接返回
		- saga：正向perform，补偿compensate
			- 注意点1 隔离性：正向操作完成后，其他事务已经能观察到了，负向回滚之后可能影响到其他事务（用户看到订单消失）
				- 考虑引入一个中间状态例如 pending
			- 注意点2 幂等：perform/compensate 都应该幂等
			- 注意点3 空回滚：没有perform就compensate；补偿前用业务主键查是否有perform，没有直接返回，并记录已回滚
			- 注意点4 事务悬挂：先compensate再perform；perform前查询是否compensate过，有则报错
		- 选型
			- 不要求实时，只要求最终一致：本地消息表、最大努力通知
			- 要求实时：TCC
			- 长事务、涉及外部/遗留系统：saga
- ch12 战略设计
	- 概念
		- 限界上下文：一个特定业务内的概念、规则、流程
			- 如电商系统中的订单、商品、营销、物流
		- 上下文映射：不同限界上下文之间的协作关系
		- 子域：关联性强的限界上下文形成的大的业务概念
			- 主播+直播 -> 视频直播子域
	- 划分限界上下文：按照业务边界
	- 上下文映射
		- 共享内核：存在共享的代码、领域模型、基础设施等
		- 客户 供应商：客户（下游）给供应商（上游）提要求，如加接口、加字段
		- 跟随者：上游不响应下游要求，下游得自己做
		- 各行其道：上下游完全不关联
	- 子域类型
		- 核心子域：业务系统最重要的部分，业务价值高
		- 支撑子域：起到支撑作用，但是没有成熟/通用方案，需要自己构建
		- 通用子域：有通用性，存在成熟/通用方案，可以通过采购/开源获得
- ch13 领域建模
	- 事件风暴法：收集所有领域事件，归纳领域模型
	- 收集内容
		- 领域事件
		- 命令：触发领域事件
		- actor：命令的人为发起者
        - 策略：命令的规则发起者，满足某种条件自动触发；定时任务也算
        - 外部系统：也是命令的发起者
        - 聚合
        - 读模型：actor 发起命令前读的数据（例如审核员查看内容），需要这些数据辅助决策
        - 热点：待定问题
    - 建模流程
        - 列举领域事件
        - 按业务流程排序领域事件；无法连接的说明可能遗漏了
        - 补充命令
        - 补充发起者
        - 提取聚合：同一个聚合的领域事件归类到一起
        - 补充读模型
        - 划分限界上下文，标注映射关系；注意需要标记上下游
        - 划分子域
- ch14 研发效能
    - maven 脚手架
    - 响应封装 graceful response
    - 对象转换 mapstruct
    - 静态分析
    - 低代码
    - 持续集成/持续交付/持续部署
- ch15 测试驱动开发 tdd
    - 红绿循环：写测试、测试失败、写代码、测试通过、重构、测试通过
	- 贫血模式的 tdd
		- dao：生成的，一般不用测
		- service：对基础设施的调用应该 mock
		- controller：mockmvc 直接测试 http 请求，验证响应（如返回码）
    - ddd 中的 tdd
		- 实体：测全分支；不应该依赖启动容器和基础设施
		- 值对象：覆盖业务规则
		- factory
- ch16 敏捷开发
	- scrum
	- 看板
- ch17 架构可视化
	- c4 模型
		- 系统上下文图：只展示核心系统、支持元素（如外部依赖、用户）
		- 容器图：展示主要数据选型和个容器的职责分工
			- 容器：可独立运行/部署的单元（如后台单体、缓存、数据库）
		- 组件图：展示可执行容器内部分工，指导开发
		- 代码图：UML/ER图，不推荐画（变更频繁）
	- 其他图
		- 系统全景图：展示关联的所有系统
		- 动态图：展示元素在运行时如何协作，用箭头和编号表示顺序
		- 部署图：说明部署方案，含有实例数量、机房等
- ch18 重构
	- 模式
		- 修缮者：实现新方法，保留老方法，用开关切换，无问题后删除老方法
		- 绞杀者：设计新系统，用一个门面承接流量，逐渐在新系统实现功能，并切老系统的流量到新系统，直到老系统完全无流量
		- 推翻重建：彻底放弃老系统
	- 流程
		- 启动
			- 必要性评估：考虑性能、可靠性、技术栈、业务支持、研发效率、运营效率
			- 环境因素：企业战略、组织架构稳定性、文化氛围、管理风格
			- 效益和风险分析
			- 可行性分析：天时（和企业战略一致）、地利（已具备环境条件）、人和（团队愿意支持思想一致）
			- 干系人识别：团队成员、管理者、职能部门负责人、最终用户
		- 规划
			- 确认重构范围
			- 工作任务分解：任务分给唯一的某个人完成，可以向其他组员寻求帮助，但是负责人只有一个
			- 工期估算和进度计划
			- 沟通计划：管理层、项目成员、外部团队
			- 人力资源计划：人力不足需要申请人力、提前培训
			- 质量管理计划：业务流程梳理用力、数据一致性对比
		- 执行
			- 组建团队
			- 梳理现有业务逻辑：读代码、读历史文档、头脑风暴
			- 整理用例并评审
			- 实施开发：DDD、敏捷、测试驱动、CICD
			- 数据迁移
			- 灰度切量
			- 老系统下线：老系统可能还有调用者
		- 监控
			- 进度
			- 质量
			- 待办项
		- 收尾
			- 沉淀过程资产：架构图、FAQ、接口文档
			- 推动新系统普及：通知调用方
	- 数据迁移的实现
		- 方案1：双写
			- 步骤1：老系统开始写入新数据源；新系统需要支持写老数据源
			- 步骤2：历史数据从老数据源全量迁移到新数据源；检查新老数据一致性
			- 步骤3：读验证，灰度部分读流量到新系统
			- 步骤4：写验证，灰度部分写流量到新系统（新系统依然写老数据源，且定期校验）
			- 步骤5：全部流量切换到新系统，新系统双写老数据源
			- 步骤6：新系统稳定后，关闭新系统写老数据源开关，老系统下线
		- 方案2：双向数据同步
			- 正向：老->新：传输老系统的所有数据
			- 反向：新->老：只传输新系统生成的数据（对来自老系统的数据有特殊标记）
			- 步骤：正向链路一直打开，全量数据从老系统迁移到新系统，且保证增量同步；写验证前打开反向链路，直到老系统下线
- ch19 布道领域驱动设计
	- 编码指南
