0. 什么是DDD
DDD是一种针对大型复杂系统的领域建模与分析方法,它是一套方法论,试图分离技术实现的复杂性,建立了以领域为核心驱动力的设计体系。
DDD的解决问题思想是将复杂的问题细分为子问题域(分而治之),再逐个解决子问题域,当解决了所有子问题域后,就建立了完整地领域模型。
DDD的正确使用步骤:
【1】采用DDD的方法开始需求分析、领域建模,主键建立起多个问题子域
【2】将问题子域落实到限界上下文中,并梳理出上下文映射关系图
【3】将各个子域落实到微服务中的贫血模型、充血模型、实体、值类型、聚合、存储库、工厂和服务的设计中,依据上下文映射图形成微服务之间的接口
1. 战略设计
战略设计,通过建立限界上下文、统一语言和上下文映射对业务进行高层次的抽象和归类。
战略设计从业务视角出发,关注复杂业务的分解,通过将复杂业务分解为一个个小的子域以及相互之间的关联关系,可以指导团队协作和后续的战术设计。
战略设计的产出一般包括限界上下文、统一语言、问题域的划分(核心域、支撑域和能用域)、上下文映射。
1.1 领域(Domain)
领域即是一个组织在一个边界内所做的事情以及其中所包含的一切,即业务知识。业务有一些内在规则,存在专业性,比如财务、CRM、OA、电商等不同领域的业务规则不同。计算机只是业务规则的自动化。
1.2 子域(Sub Domain)
子域是领域的一部分。
在DDD中,为了降低业务理解的复杂度,一个领域被划分为若干子域,每个子域都有一个清晰的限界上下文,领域模型在限界上下文中完成开发。在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。
子域包括核心域、支撑域和通用域,将子域划分成不同的类型主要考虑到,公司在 IT 系统建设过程中,由于预算和资源有限,对不同类型的子域应有不同的关注度和资源投入策略。
1.3 通用语言(Ubiquitous language)
通用语言是团队自己创建的公用语言,其中包括领域专家、开发、业务分析员等。在同一个限界上下文中,团队成员使用通用语言进行交流。通用语言会随着时间推移而不断的演化。
通用语言包含术语和用例场景,并且能够直接反映在代码中,有助于将业务需求直接转化为代码。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。
限界上下文与通用语言存在一对一的关系。
1.4 核心域(Core Domain)
核心域是指领域中最核心的部分,通常对应企业的核心业务和核心竞争力,也是业务成功的主要促成因素。
从战略层面上讲,应该给核心域最高的优先级、最资深的领域专家和最优秀的开发团队。
1.5 支撑域(Support Domain)
支撑域是一种特殊的子域,是指为了实现核心业务而不得不开发的业务所对应的相关知识的集合。支撑域对应着业务的某些重要方面,类似于定制开发,但却不是核心。对它的投入无论如何也达不到与核心域相同的程度,甚至可以考虑使用外包的方式实现此类限界上下文,
例如,活动平台业务属于电商的支撑域,因为该业务对于电商企业并不是必需的,其存在的意义仅在于放大利润。
1.6 通用域(General Domain)
通用域是另一种特殊的子域,没有太多的个性化述求,对应的是业界已经有成熟方案的业务。
通用域可以看做一种特殊的支撑域,可以使用标准部件来实现,短信通知、邮件等领域问题。
1.7 限界上下文
限界上下文包括限界和上下文两个词,限界代表领域的边界,上下文代表通用语言的语境,综合表示应用程序的一个概念性边界。在这个边界之内的每种领域术语、词组或句子–即通用语言,都有确定的上下文含义。在边界之外,这些述语可能表示不同的意思。
在 DDD 实践中领域模型会被限定在限界上下文当中。
限界上下文强调概念的一致性。虽然传统的方法学已经在追求概念的一致性,但是忽略了系统的庞大性,不论系统多庞大,在系统任何位置同一概念通用。DDD 不追求全局的一致性,而是将系统拆成多块,在相同的上下文中实现概念一致性。
识别上下文可以从概念的二义性着手,比如商品的概念在物流、交易、支付含义完全不一样,但具有不同内涵和外延,实际上他们处在不同上下文。
限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容,如应用服务、数据库Schema。
限界上下文可以用于微服务划分、避免模型的不正确复用带来的问题。
1.8 问题空间(Problem Space)
业务所面临的挑战。
问题空间是领域的一部分,对问题空间的评估应该同时考虑已有子域和额外所需子域。
1.9 解决方案空间(Solution Space)
解决业务所面临的挑战的解决方案集合。
解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。
1.10 上下文映射(Context Map)
上下文映射表示不同限界上下文在解决方案空间中是如何通过集成相互关联的。
其中U代表upstream,D代表downstream,下游依赖上游。
在上下文映射中需要特别注意循环依赖、双向依赖和过长的依赖,如果出现这几种依赖关系,需要思考限界上下文分解的是否合理。
限界上下文之间的低耦合是指限界上下文之间通过接口调用,如C上下文依赖A上下文和B上下文,只要B上下文实现了A上下文的接口,C上下文可以只依赖B上下文。
2. 战术设计
战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
2.1 实体
实体是具有唯一身份标识的对象。
实体的特征:
- 具有唯一身份标识并且在实体的生命周期内保持不变
- 可变性(延续性)
- 具有相同身份标识的两个实体是相同的对象
- DDD中采用充血模型实现
//实体
public class Product extends Entity {
//实体的身份标识为一个值对象
private ProductId productId;
private ActivePolicy activePolicy;
...
public Product(ProductId productId) {
this.setProductId(productId);
}
public Date creationDate() {
this.productId().creationDate();
}
//省略setter和getter
}
//值对像
public class ProductId extends ValueObject{
private static final SimpleDateFormat DEFAULT_DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");;
private String productId;
public ProductId(UUID uuid) {
this.productId = "APM-P-" + DEFAULT_DATE_FORMATTER.format(new Date()) + "-" + uuid.substring(0,10)
}
public Date creationDate() {
return DEFAULT_DATE_FORMATTER.parse(this.productId.split("-")[2]);
}
}
//获取ProductId
public interface ProductRepository {
default ProductId nextId() {
return new ProductId(UUID.randomUUID());
}
2.2 值对象
值对象表示属性集合,将多个相关属性组合为一个不可修改的概念整体。
值对象的特点:
- 一般没有身份标识
- 不可变(只能整体更新,实际上是替换成了一个新的值对象实例)
- 具有相同属性值的两个值对象可以互换使用
- 只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑
- 逻辑上通常作为实体的一部分,用于描述实体的特征
2.3 领域服务
领域服务表示一个无状态的操作,它用于实现特定于某个领域的任务。
领域服务主要用于如下场景:
- 执行一个显著的业务操作过程(如计算金额、审计等)
- 对领域对象进行转换
- 跨越个领域对象(实体)
在实现领域服务时,如果一个领域服务只有一种实现且并非一个技术上的实现(如与其他服务或基础设施进行集成等),通常没有必要为领域服务声明一个接口。
2.4 领域事件
领域事件表示领域中所发生的事情,通常会导致进一步的业务操作。每个事件用领域对象表示。
领域事件的产生、存储、转发和订阅:
领域事件的适用场景:
- 通过事件维护数据的最终一致性
- 将复杂的或集中处理的业务逻辑拆分成许多粒度较小的处理单元(分治法)
- 降低服务的直接访问压力
领域事件建模:
- 领域事件一般用于表示一个已经执行成功的操作,通过由命令操作产生
- 领域事件代表已经发生的事情,即命名上采用过去式,一般的全名范式为“聚合+操作的过去式”
- 领域事件对象通常被设计成不可变的,通过构造函数进行全状态的初始化
领域事件的基本属性:
- 事件唯一标识(全局唯一)
- 发生时间
- 事件类型
- 事件源
2.5 模块
模块表示一个命名的容器,用于存放领域中内聚在一起的类。模块应该包含一级具有高内聚的概念集合,将类放在不同模块中的目的在于达到松耦合性。
设计模块的简单原则:
- 模块应该和领域概念保持一致,并根据通用语言来命名模块
- 设计松耦合的模块
- 同层模块应该杜绝循环依赖
- 父子模块之间可以放松原则
- 模块应该随着模型的变化而变化,并不是一层不变的
- 不要机械的根据通用的组件类型和模式来创建模块,如service
- 对于内聚性不强或者没有内聚性的领域对象来说,应该将它们划分到不同的模块中
示例:
- com.thoughtworks(组织顶级域名).agilepm(限界上下文).domain(分层).model(模块).team(具体模块)
- com.thoughtworks.agilepm.domain.model.project
- com.thoughtworks.agilepm.domain.model.product
- com.thoughtworks.agilepm.domain.model.product.backlogitem
- com.thoughtworks.agilepm.domain.model.product.release
- com.thoughtworks.agilepm.domain.model.product.sprintt
2.6 聚合
聚合由多个领域对象(实体和值对象)在一致性边界之内组成(概念上体现的是整体与部分的关系,代码实现上体现在聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储)。定义一个聚合通常包括两部分:
- 一个聚合根实体(Aggregate Root),一般用聚合根代表聚合
- 聚合的完整性规则
聚合的完整性规则通常包括:
- 在一致性边界内业务的不变条件,如订单的价格与订单项
- 所有的代码只能通过聚合根访问系统的 Entity,而不能随便的操作任一的 Entity
- 每个事务范围只能只能更新一个聚合根及它所关联的 Entity 状态
设计聚合的注意事项:
- 设计小的聚合:使用聚合根(一个Entity)来表示聚合,其只包含一些基本数据类型属性和值对象属性(值对象可以和聚合根存储在一张表中),只要能够满足业务不变条件即可,不建议一开始就设计成大聚合
- 聚合间的引用:通过唯一标识引用其他聚合,被引用的聚合不在一致性边界内(即在同一个事务中进行修改)
- 最终一致性:如果一个事务中需要修改多个聚合,那么可以借助事件实现跨聚合的最终一致性
- 聚合的加载:在Application service层中使用资源库加载聚合
- 估算聚合成本:从一个命令需要聚合加载的对象的数量(或平均数量)来考虑聚合的成本
- 考虑用例场景:从可扩展性、并发性角度思考用例场景
- 应用服务:跨多个聚合的业务逻辑通过应用服务来实现,为了未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联
- 领域服务:跨多个实现的业务逻辑同类领域服务来实现
2.7 聚合根
聚合根主要为了解决复杂业务模型由于缺少统一的完整性规则,而导致的聚合实体间的数据不一致问题。
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
聚合根的作用包括:
- 作为实体,具有实体的属性和业务行为,能够实现业务逻辑
- 作为聚合的管理者,在聚合内部直接引用实体和值对象,负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑
- 在聚合之间,是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同
2.8 存储库
通过存储库(Repository)完成对数据库的访问。
与DAO的区别:
- DAO保存和查询的都是单个表
- 一般每个表都有一个DAO
- 存储库根据聚合中的领域对象的关系保存和查询数据,一次性可以操作多个表,并且对于聚合数据的保存都是事务性的
- 存储库具有缓存领域对象的功能,DAO一般借助数据库或ORM的缓存功能
2.9 工厂
注意与工厂设计模式的差别,工厂设计模式是降低调用方与被调用方的耦合度,而DDD的工厂是为了装配领域对象,是领域对象生命周期的起点。
分别调用各个DAO获得相应的数据,再将这些数据组装成领域对象。
通过存储库与工厂,对原有的DAO进行了一层封装,在保存、装载、查询等操作中,加入聚合、装配等操作,并将这些操作封装起来,对上层的客户程序屏蔽。