领域建模重要概念
在阅读之前应该理解了DDD的主要思想和 限界上下文,子域核心域等基础概念
实体(Entity)
- 重要概念:标识,可能由唯一一条属性,或多种属性组合而成。
概念性解释
一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread of Identity),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具 有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。
主要由标识定义的对象被称作ENTITY。ENTITY(实体)有特殊的建模和设计思路。它们具 有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了 有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标识来 决定,而不依赖于其所具有的属性。即使对于那些不发生根本变化或者生命周期不太复杂的 ENTITY,也应该在语义上把它们作为ENTITY来对待,这样可以得到更清晰的模型和更健壮的实现。
当然,软件系统中的大多数‚ENTITY‛并不是人,也不是其通常意义上所指的‚实体‛或‚存 在‛。ENTITY可以是任何事物,只要满足两个条件即可:
- 一是它在整个生命周期中具有连续性
- 二是它的区别并不是由那些对用户非常重要的属性决定的。
比如小明想去看一场演唱会,买了一张门票,门票上写着座位号为 4排3座 ,那么该座位在建模时应当是实体,因为小明应该只能坐 4排3座 的座位,其他位置的座位不属于他。此时 4排3座就是一个唯一性标识。但如果演唱会门票改为入场券形式,即门票上只说明可以凭此票拥有一个座位,那么此时的座位就不再是实体,而应该是“值对象”,也就是说每个座位都是一样的,只是用来坐而已。后一节我们将会详细了解值对象的概念。
注意:将与标识有关的属性留在Entity里,将无关行为和属性转移到与核心实体关联的其他对象中。
识别时注意:是否可以互换?
很多对象不是通过它们的属性定义的,而是通过连续性和标识定义的。
创建策略
创建实体身份标识的策略,从简单到复杂:
- 用户提供一个或多个初始值作为程序输入,应该保证这些初始值是唯一的
- 程序自身通过一些算法、类库、框架生成唯一标识
- 持久化存储时,如数据库存储时,来生成唯一标识
- 另一个限界上下文决定出了唯一标识,可以作为程序输入
总结重要属性
- A1: 具有唯一标识(Identity)唯一标识可以是一条以上的属性,有四种生成途径,不同的生成时间
- A2: 在整个生命周期中连续
- A3: 拥有其他 属性(Attribute) 和 实体行为(Operation)
- A4: 使用领域事件和事件存储跟踪连续性变化
值对象(Value Object)
- 很多对象没有概念上的标识,它们描述了一个事务的某种特征
- 不变的对象可以自由的共享,如需改变只能整体替换
概念性解释
用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。VALUE OBJECT被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不 关心它们是谁。
VALUE OBJECT可以是其他对象的集合。在房屋设计软件中,可以为每种窗户样式创建一个对象。我们可以将“窗户样式”连同它的高度、宽度以及修改和组合这些属性的规则一起放到“窗户”对象中。这些窗户就是由其他VALUE OBJECT组成的复杂VALUE OBJECT。它们进而又被合并到更大的设计元素中,如“墙”对象。
VALUE OBJECT经常作为参数在对象之间传递消息
VALUE OBJECT可以用作ENTITY(以及其他VALUE)的属性
VALUE OBJECT应该是不可变的
对值对象的持久化存储
如果一个实体持有值对象集合引用,该如何去存储呢?例如,在权限管理系统中,一个用户A,持有值对象集合{创建者,编辑者,查看者},每个值对象也是立体的对象,包括各种属性,那么如何将这种关系持久化存储呢?
-
多个值对象序列化到单个列中
- 即把各个角色对象(创建者等)拼接起来,存到单独的列中,但
- 列宽有所限制
- 如果拼接的属性需要充当查询条件,会比较麻烦
- 如果拼接的属性比较复杂,要自定义类型来做
-
使用数据库实体保持多个值对象
- 这里实际上是以数据建模的角度把值对象当作实体保存,但值对象在领域模型中依然还是值对象
- 举个例子,用户A的id为1,这个id是用来做唯一标识和跟踪的,有了这个id1,用户A就是一个实体
- 采用持久化委派主键的方法,让用户A下的值对象(创建者等)也拥有其id作为属性,只在存储到数据库时才生效
-
使用联合表保存多个值对象
- 值对象保存到另一个表中
- 再用外键关联到实体的唯一标识上
总结重要属性
- A1: 不变性(一旦创建,不可修改,只能替换)
- A2: 不是领域中的一个东西,只是用于度量或描述领域中某个东西的概念
- A3: 有意义整体(500美元,500和美元组合起来才有意义)
- A4: 相等性(所有属性值相等则两个值对象相等)
领域服务(Service)
-
强调与其他对象的关系
-
往往以一个活动来命名,是动词而不是名词
概念性解释
在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是SERVICE(服务)。
好的SERVICE有以下3个特征:
- 与领域概念相关的操作不是ENTITY或VALUE OBJECT的一个自然组成部分。
- 接口是根据领域模型的其他元素定义的。
- 操作是无状态的。
领域服务是用来协调领域对象完成某个操作,而不属于某个对象;但与对象关系紧密,需要参照对象来设计;用来处理业务逻辑,它本身是一个行为,所以是无状态的。状态由领域对象(具有状态和行为)保存。
上面也说了,领域对象是具有状态和行为的。那就是说我们也可以在实体或值对象来处理业务逻辑。那我们该如何取舍呢?
一般来说,在下面的几种情况下,我们可以使用领域服务:
- 执行一个显著的业务操作过程
- 对领域对象进行转换
- 以多个领域对象为输入,返回一个值对象。
比如在银行系统中,转账应该是一个领域服务,它包含了很多步骤,涉及转账方余额校验,收款方是否符合规定,扣款,余额增加,保持事务,转账失败回滚等等业务领域的操作,这些操作无法直接被某个领域对象完全收敛,所以选择以领域服务进行实现。
与应用服务不同
注意:要将领域服务和应用服务进行区分!
应用服务是用来表达用例和用户故事(User Story)的主要手段。
应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。
除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。
应用层作为展现层与领域层的桥梁。展现层使用VO(视图模型)进行界面展示,与应用层通过DTO(数据传输对象)进行数据交互,从而达到展现层与DO(领域对象)解耦的目的。
比如转账的一系列操作,当转账完成后,发送短信通知用户,这个行为我们更偏向于将它建模为应用服务。很显然,这个发送短信与转账这一业务没有关系,我们要确保领域服务只关心业务逻辑即纯粹性。
不要过度使用领域服务
过度使用领域服务将会产生一个贫血模型,例如数据建模时,我们的实体常用只含有get/set方法,所有的业务逻辑都包含在了service。这样导致service变成了一个大泥球。注意区分领域服务与实体,值对象行为。
总结重要属性
- A1: 无状态的
- A2: 输入:多个领域对象;输出:一个值对象
- A3:以一个活动命名,是动词而非名词
领域事件
概念性解释
- 领域事件通常是用来与其他聚合解耦的,采用观察者模式,一个聚合订阅另外一个聚合的事件。
领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。
- 由于不同的聚合之间通讯主要采取聚合根进行,所以有必要采用领域事件作为媒介
- 可以采用消息总线来进行实现,需要接收变化的聚合根注册到事件总线上,一旦发生变化,通知所有注册的聚合根
- 如果是同一个限界上下文时,通常采用进程内的消息发布领域事件;如果是在不同限界上下文时,我们可以采用消息中间件,比如rocket mq,kafka进行消息的订阅与发布。
领域事件更加关注最终一致性,而不是原子性。
可以对照 事件总线 来进行理解。
领域事件经常被用来充当不同聚合间交换信息的媒介,我们知道聚合的内部是统一的,但有时也要保证不同聚合间的一致性,但又不能直接去做,因为让不同聚合内部的属性同步,违反了聚合的设计理念,所以用领域事件来达到最终一致性。
总结重要属性
- A1: 由聚合上的命令产生,命名反映已经发生的事件
- A2: 具有唯一身份标识(Identity)
- A3: 包括发起方 和 参与者
- A4: 可以通过调用领域服务创建
参考资料:
作者:『圣杰』
出处:http://www.cnblogs.com/sheng-jie/