DDD基本元素
Entity
以Identity作为其基本定义的对象,其存在形式和内容可以发生很大变化,但区分不同Entity的唯一准则就是Id。
Entity对象并不主要是由它们的属性来定义。它们体现了标识在时间上的延续性,经常要经历多种不同的形态。有时,一个对象与另一个对象有不同的属性,但它们却是相互匹配的;有时一个对象与其他对象有着相同的属性,但它必须能够跟那些对象区分开来。
比如在某个系统中Person被辨别为Entity:两个人都叫张三,但他们是不同的Person;一个人小时候叫张三,但后来觉得这个名字不好,就改名为张三丰,这是一个Person的属性发生了变化。
唯一Id的生成是一个需要注意的问题,比如用数据库自增的方式产生的Id,对于在分布式系统甚至是跨系统的实现来说都不是“唯一”的,这会引发很多问题。
Value Object
如果一个对象代表了领域的某种描述性特征,并且没有概念性的Id,我们就称之为ValueObject。
设计值对象时需要对复制、共享和不变性做出选择,为了能够尽量利用共享带来的好处,同时避免它的缺陷,只在以下情况中使用共享:
- 当存储空间和对象数量有严格限定时
- 当通信开销不高时(例如在一个中心服务器上)
- 当共享对象具有严格不变性时
尽量将值对象设计成不可更改的,除非:
- 值经常被改变
- 对象的生成和删除开销很大
- 替换(不是修改)会打破集群
- 值没有太多的共享,或共享并没有提高集群性能等其他技术原因
Service
领域中的一些概念不能作为模型中的对象来处理,而是作为一种接口来提供操作,它独立于模型,没有像Entity和ValueObject那样封装状态。
服务代表了一种行为而不是一个实体。一个优秀的服务具备3种特征:
- 与领域概念相关的操作不是实体和值对象中固有的部分
- 接口根据领域模型中的其他元素来定义
- 操作是无状态的
要区分Application、Domain和Infrastructure服务,以资金转账为例:
Application层
- 读取输入(例如XML请求)
- 发送消息给领域服务,要求处理
- 监听确认消息
- 决定用Infrastructure的服务发送通知
Domain层
- 必要的账户和分类账对象的相互作用,完成正确的提取和存入
- 确认转账结果
Infrastructure层
- 由应用选择通知方法,发送电子邮件、信件或通过其他通信途径
Aggregate
一个聚合是一簇相关联的对象,出于数据变化的目的,我们将这些对象视为一个单元。每个聚合都有一个根和一个边界。边界定义了聚合中包含什么,根则是聚合中唯一允许被外部对象引用的元素,是单个特定的Entity。
- 聚合根具有全局标识,并最终负责对不变量的检查
- 聚合边界以外的任何对象除了可以引用根,不能持有任何对其内部对象的引用。根可以把其内部实体的引用传递给其他对象,但是它们只能临时使用这种引用,而不能持有这种引用。它还可以复制一个值对象的副本传给另一个对象。它并不关心这个副本会发生什么变化,因为那只是一个值,而且与聚合已经不再有任何关联。
- 能通过数据库查询直接获得的对象只有聚合根,所有其他对象必须通过导航关联来访问。
- 聚合内的对象可以持有其他聚合根
- 删除操作必须一次性删除聚合边界内的所有对象
- 当在聚合边界内发生的任何对象修改被提交时,整个聚合的所有不变量必须都被满足
不变量(Invariant)是指无论何时数据发生变化都必须满足的一致性规则。不能指望涉及聚合的任何规则都是每时每刻被满足的,但是聚合内部的不变量必须在每次事务完成时被满足。
Factory
用于Entity以及Aggregate的创建,尤其是创建Aggregate的时候最好使用Factory。实现形式可以是Factory Method、Abstract Factory或者Builder。
在以下情况下可以不用Factory而是直接用构造函数:
- 要创建的类型不是层次结构的一部分,没有多态的情况。
- 客户关心创建时的具体实现
- 客户可以使用对象的所有属性,因此其构造函数没有嵌入对象创建的逻辑
- 创建过程非常简单
- 构造函数是一个原子操作,创建出来的对象满足所有不变量
Repository
仓储将某种类型的所有对象描述为一个概念性的集合,而且包含精细的查询能力。仓储可以加入和删除具有合适类型的对象,为我们提供了对聚合根从产生之初到生命周期结束期间的访问能力。一般除了保存、更新和删除操作以外,仓储还可以提供标准化的查询接口比如GetById()、具体条件查询接口如GetByName(string name)以及更通用的查询接口GetByCriteria(Criteria c),如果需要,仓储也可以提供“有多少符合条件的对象数”或“某数值的累加和”等统计操作接口。
仓储具有很多优点:
- 为客户提供了一个简单的模型,来获取持久对象并管理其生命周期
- 把应用和领域设计从持久技术、数据库策略甚至多种数据来源解耦
- 传达对象访问的设计决策
- 可以很容易的被替换成Mock实现,以便在测试中使用
实现时的关注点:
- 返回抽象类型、基类型
- 与客户代码解耦
- 让客户端代码来控制事物
Domain Event
有时Entity需要与Service和Repository打交道,如果把这样的业务放入Service,则容易造成贫血的实体,如果把Service或Repository注入到Entity中又会使得Entity反过来依赖Service或Repository,非常丑陋。所以为了解决这一问题有人提出DomainEvent的概念。通过Event Observer或者Message Bus,将一个Entity与Service、Repository解耦。
Module
当系统复杂到一定程度后,需要对Domain进行模块划分,划分的依据是高内聚、低耦合。模块提供了两种分析模型的方法:以模块为单位考虑它的实现细节,或从全局分析模块之间的联系而不用关心细节。