【领域驱动设计模式、原理与实践】圈定范围
DDD 是让你的代码与问题域保持一致的处理过程。随着你的产品的演化,添加新的特性会变得像之前在全新开发阶段一样容易。
更新历史
- 2022.12.20:完成初稿
读后感
这是一部七百多页的大部头,里面更加深入介绍了 DDD 具体如何在项目中使用,框定了很多使用范围,是一本不错的补充书籍,可以用于快速落地。
读书笔记
前言
编写软件很容易——至少开发全新的软件是这样的。当你要修改由其他开发人员编写的代码或者你六个月之前编写的代码时,最乐观的情形可能是有点无聊,但最糟的情况是一场噩梦。DDD 是让你的代码与问题域保持一致的处理过程。随着你的产品的演化,添加新的特性会变得像之前在全新开发阶段一样容易。
什么是领域驱动设计
领域驱动设计的实践与原则:
- 专注于核心领域
- 通过协作进行学习
- 通过探索和实验来创建模型
- 通信
- 理解模型的适用性
- 让模型持续发展
要点:
- DDD 并不会为开发强制规定任何特定的架构样式,它只会确保模型与技术复杂性保持分离,这样它就能专注于领域逻辑方面的内容。
- DDD 并非一种模式语言,它是专注于交付的一种协作思想体系,其中通信起着核心作用。
- DDD 是进行软件开发的一种以语言和领域为中心的方式。
提炼问题域
要点:
- 知识提炼是处理领域信息以识别出能用于构建有用模型的相关碎片知识的技术。
- 知识是由开发人员与领域专家协作而获得的。协作有助于弥补所有的知识漏洞并促成共识。
- 共识是通过一种称为通用语言(UL)的共享语言来达成的。
- 知识提炼是一个持续过程:协作和业务人员的参与不应该局限于项目的初始阶段。深层见解和突破仅会产生于为解决问题而进行的多次开发迭代之后。
- 知识是团队所有成员在项目任何时点围绕白板讨论、在人们放松闲谈的场所进行讨论、头脑风暴以及用协作方式制作原型的过程中得到的。
- 领域专家是组织的主题专家。他们是哪些能够提供问题域中见解的人(用户、产品所有者、业务分析员、其他的技术团队。
- 要有计划地修改你的模型;不要太投入,因为知识提炼中的突破可能会被废弃。
- 要围绕系统最重要的用例驱动知识提炼。要寻求领域专家遍历系统用例的具体场景,以便帮助弥补知识漏洞。
- 提出有力的问题并了解业务的意图。不要仅仅实现一组需求,而要主动让业务人员参与其中;与他们一同工作,而不是为他们工作。
- 在代码中进行实验以证明模型的有效性,并提供出于技术原因需要对模型进行修改的反馈。
- 查看行业中已有的处理过程和模型,以避免重新发明车轮的情况以及加速领域知识的获取。
- 找出你不知道的内容,尽早识别出团队的知识漏洞,然后启用刻意发现。消除不了解的未知知识并尽早增长领域知识。
专注于核心领域
要点:
- 提炼用于分解大的问题域以便找出核心、支撑和通用域。
- 提炼有助于降低问题空间的复杂性。
- 专注和经历应该投入到核心领域。在此处使用你最好的开发人员。
- 要考虑外包开发、采购支撑和通用域或者将初级开发人员放到支撑和通用域。
- 一份领域愿景说明会揭示对产品取得成功的核心内容的共识。使用领域专家、项目启动文档以及业务战略介绍来帮助发出领域愿景说明的通告。
模型驱动设计
预先设计的问题
团队建模优化预先设计
要点:
- 模型是由分析模型和代码模型来表示的。它们完全是一回事。
- 分析模型只有在它保持与代码模型协同时才有用。
- 开发 UL 的过程都是 DDD 最重要的一环,因为它能促进沟通和学习。
- 必须明确定义领域专业术语以确保含义准确,因为沟通中使用的专业语言会被融入代码实现中。
- 领域充满了以清晰、准确方式描述复杂概念的特殊术语和语言。
- 通用语言 UL 应该用于测试、命名空间、类名称和方法。
- 仅为核心领域应用模型驱动设计并创建 UL 才能带来变化。不要将这些实践应用到整个应用程序。
使用有界上下文维护领域模型的完整性
要点:
- 领域层包含领域模型,且与基础架构和呈现问题相分离。
- 尝试将单个模型用于复杂问题通常会导致代码变成大泥球。
- 一个有界上下文就是一个语言边界。它会隔离模型以免除 UL 中的歧义。
- 有界上下文会保护领域模型的完整性。
- 单个团队应该拥有一个有界上下文。
- 架构模式是在有界上下文的级别而非应用程序级别应用的。如果有界上下文中没有复杂逻辑,则可以使用简单的 CRUD 架构。
团队开始应用领域驱动设计通常会遇到的问题
- 过分强调战术模式的重要性
- 将相同架构用于所有的有界上下文
- 力求战术模式尽善尽美
- 错误估计构造块对于 DDD 的价值
- 专注于代码而非 DDD 的原则
- 缺失了 DDD 的真实价值:协作、通信和上下文
- 由于低估上下文的重要性而产生大泥球
- 未能成功创建 UL 将造成歧义和误解
- 由于缺乏协作只能设计专注于技术的解决方案
- 在不重要的部分花费太多时间
- 简单问题复杂化
- 将 DDD 原则应用到具有少量业务预期的琐碎领域
- 别将 CRUD 作为反模式
- 将领域模型模式用于每一个有界上下文
- 问一问自己:额外的复杂性是否值得
- 低估应用 DDD 的成本
- 尝试在没有积极专注的团队的情况下取得成功
- 项目背后没有领域专家时的协作尝试
- 在非迭代式开发方法中进行学习
- 将 DDD 应用到每一个问题
- 为不必要的纯粹性而牺牲实用主义
- 寻求验证会浪费精力
- 永远力求代码之美
- DDD 关乎的是提供价值
应用 DDD 的原则、实践与模式
要点:
- 不要把 DDD 当做灵丹妙药来推广使用。要专注于与业务保持一致并且学习与正在为之构建软件的领域有关的更多知识。
- 仅在需要时应用 DDD 原则。不要将其用作解决所有问题的工具。
- 分解空间问题并专注于核心领域。所有利益相关的对话都在此处发生。这就是应用 DDD 原则以便最大化价值的地方,也是应该投入最大精力的地方。
- 持续集成、改进和挑战模型。不要止步于第一个好想法。通过尝试新的模型和解决方案来探究和实验,并且验证好的想法。至少要有 3 个有用模型。
- 不要作任何假设,保持事物的简单性,推迟进行大的设计决策,等待复杂性或新的行为挑战解决方案。然后在需要时朝着战略模式的方向进行重构。
- 模型和语言要一同演化。不能轻易交流和谈论的模型其实用性将很有限并且将很难对其演化。
构建块领域建模介绍
要点:
- 实体
- 由其身份定义
- 身份在其整个生命周期保持不变
- 负责相等性检查
- 值对象
- 描述问题域中的属性和特征
- 不具有身份
- 是不可变的,意味着它们不能被变更。相反,必须替换作为值对象建模的属性
- 领域服务
- 包含无法自然放置在实体或值对象中的领域逻辑,而应用程序服务会编排领域逻辑的执行,但不会实际实现它
- 不具有内部状态,因此可以使用相同的输入重复调用它,而它总是会给出相同输出
- 模块
- 用于分解、组织和提高领域模型的可读性
- 命名空间,模块的一种实现,可以用来降低耦合并提高领域模型中的内聚力
- 让阅读者能快速理解模型的设计
- 帮助定义领域之间的清晰边界
- 封装可以彼此独立理解的概念。它们是在比聚合和实体更高层次的抽象上操作的
- 聚合
- 将大对象图分解成小的领域对象集群,以降低领域模型技术实现的复杂性
- 表示领域概念,而不仅仅是领域对象的通用集合
- 其基础是围绕领域不变条件的
- 是一个一致性边界,以确保领域模型保持在一个可靠状态
- 确保在核实的粒度级别设置事务性并发边界,以通过避免数据库层的阻碍来确保应用程序可用
- 工厂
- 将使用与构造分离
- 封装复杂实体和值对象构造
- 存储库
- 公开聚合根在内存中的集合的接口
- 不应用于报告
- 提供聚合根的检索和持久化需要
- 将领域层从数据库策略和基础架构代码中解耦
- 领域事件
- 是业务用户关心的现实世界问题域中的重大事件;它们是通用语言(UL)的一部分
- 是让领域事件在代码中更明确的一种显露设计模式
- 类似于发布-订阅,事件发生而事件处理程序会处理它们
- 事件溯源
- 使用生成当前状态的事件的全部历史记录替换传统的快照式存储
- 允许在运转时循环的强大查询能力,称为时态查询
值对象
要点:
- 值对象是 DDD 的建模结构体,它们表示像幅度和度量这样的描述性数量
- 由于不具有身份,因此你不必让值对象负担与实体有关的复杂性
- 值对象的示例包括 Money, Currency, Name, Height 和 Color。不过,重要的是记住一个领域中的值对象可能是另一个领域中的实体,反之亦然
- 值对象是不可变的;它们的值不能修改
- 值对象是内聚的;它们可以封装多个特性来完整封装单个概念
- 可以组合值对象来创建新的值,而无须修改原始值
- 值对象是自验证的;它们绝不应该处于无效状态
实体
要点:
- 实体是在问题域中具有唯一身份的领域概念
- 具有生命周期同样也是区分实体的一个特征
- 选择一个实体的 ID 是一个基本的实现问题
- 实体应该总是对指定上下文有效
- 要谨防将物理概念建模为单一实体。典型的 Customer 实体在逻辑上通常会被跨多个有界上下文划分到许多实体中
领域服务
要点:
- 过多的领域服务会导致贫血领域模型,它与问题域无法很好地配合
- 过少的领域服务会导致逻辑被不正确地定位到实体或值对象上。这会造成不同的概念被会和起来,从而降低清晰度