读书记录《悟道领域驱动设计》
-
阅读
- ch2 应用架构
- 贫血模型 vs 充血模型
- 贫血模型:类只是数据容器,没有行为(例如 pojo)
- 充血模型:类有属性(数据),也有方法(行为)
- 项目结构
- 接口层:对外暴露api/消息consumer
- 应用服务层:协调领域模型完成业务逻辑
- 基础设施层:写db/缓存/调外部服务
- 领域层:领域内的具体逻辑
- 查询的两种实现
- 1 读数据后加载领域模型(聚合根),聚合根转换为查询结果 view
- 2 直接用实际存储的数据模型,跳过领域对象加载,直接用把数据模型转换为查询结果 view
- ch3 实体和值对象
- 实体:有唯一标识的领域模型
- 值对象:没有唯一标识,需要的时候随时构造,值一样就认为是相同的对象
- e.g. 内容发布系统的文章标题
- DP:Domain Primitive 领域内的基本数据类型,值对象的一种
- ch4 聚合与聚合根
- 聚合:一组对象组成的对象树
- 单个实体也是聚合
- 聚合是一致性的边界;聚合内强一致,跨聚合最终一致
- 聚合根:这个对象树的入口
- 单个实体也可以是聚合根
- 外部对象只能引用聚合根(通常通过持有一个id)
- 拆分聚合
- 一个实体被多个聚合根引用,应该被提升为聚合根
- 1:N 的集合属性(Role-Resource Binding),因为存在双向查找的需要(Role找Resource,Resource找Role),通常也可能被提升为聚合根
- ch5 Factory, Repo, 领域服务
- Factory:从无到有创建领域对象
- Repo:保存和加载聚合根,和实际的存储解耦
- save:聚合根持久化到数据库
- 注意事务控制,所有实际的写操作应该在一个事务内执行(例如文章写一个表,文章内容写另一个表)
- load:从数据库查询数据对象,组装回聚合根
- 推荐实现行级 repo,一个聚合根对应数据库中一行数据
- 如果存在查多行的需求,如 queryList,应该用 CQRS 分离出去
- 领域服务:包含领域中不适合放在实体/值对象的业务操作,应该是无状态的
- 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 隔离性:正向操作完成后,其他事务已经能观察到了,负向回滚之后可能影响到其他事务(用户看到订单消失)
- 注意点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 敏捷开发
- ch17 架构可视化
- c4 模型
- 系统上下文图:只展示核心系统、支持元素(如外部依赖、用户)
- 容器图:展示主要数据选型和个容器的职责分工
- 容器:可独立运行/部署的单元(如后台单体、缓存、数据库)
- 组件图:展示可执行容器内部分工,指导开发
- 代码图:UML/ER图,不推荐画(变更频繁)
- 其他图
- 系统全景图:展示关联的所有系统
- 动态图:展示元素在运行时如何协作,用箭头和编号表示顺序
- 部署图:说明部署方案,含有实例数量、机房等
- ch18 重构
- 模式
- 修缮者:实现新方法,保留老方法,用开关切换,无问题后删除老方法
- 绞杀者:设计新系统,用一个门面承接流量,逐渐在新系统实现功能,并切老系统的流量到新系统,直到老系统完全无流量
- 推翻重建:彻底放弃老系统
- 流程
- 启动
- 必要性评估:考虑性能、可靠性、技术栈、业务支持、研发效率、运营效率
- 环境因素:企业战略、组织架构稳定性、文化氛围、管理风格
- 效益和风险分析
- 可行性分析:天时(和企业战略一致)、地利(已具备环境条件)、人和(团队愿意支持思想一致)
- 干系人识别:团队成员、管理者、职能部门负责人、最终用户
- 规划
- 确认重构范围
- 工作任务分解:任务分给唯一的某个人完成,可以向其他组员寻求帮助,但是负责人只有一个
- 工期估算和进度计划
- 沟通计划:管理层、项目成员、外部团队
- 人力资源计划:人力不足需要申请人力、提前培训
- 质量管理计划:业务流程梳理用力、数据一致性对比
- 执行
- 组建团队
- 梳理现有业务逻辑:读代码、读历史文档、头脑风暴
- 整理用例并评审
- 实施开发:DDD、敏捷、测试驱动、CICD
- 数据迁移
- 灰度切量
- 老系统下线:老系统可能还有调用者
- 监控
- 收尾
- 沉淀过程资产:架构图、FAQ、接口文档
- 推动新系统普及:通知调用方
- 数据迁移的实现
- 方案1:双写
- 步骤1:老系统开始写入新数据源;新系统需要支持写老数据源
- 步骤2:历史数据从老数据源全量迁移到新数据源;检查新老数据一致性
- 步骤3:读验证,灰度部分读流量到新系统
- 步骤4:写验证,灰度部分写流量到新系统(新系统依然写老数据源,且定期校验)
- 步骤5:全部流量切换到新系统,新系统双写老数据源
- 步骤6:新系统稳定后,关闭新系统写老数据源开关,老系统下线
- 方案2:双向数据同步
- 正向:老->新:传输老系统的所有数据
- 反向:新->老:只传输新系统生成的数据(对来自老系统的数据有特殊标记)
- 步骤:正向链路一直打开,全量数据从老系统迁移到新系统,且保证增量同步;写验证前打开反向链路,直到老系统下线
- ch19 布道领域驱动设计