上一篇:DDD 领域驱动设计-三个问题思考实体和值对象

说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料,真正去写的时候,才发现这类的博文真不是一般的难写,一句话要反复揣摩,并进行理解,最重要的是半天才蹦出一句话。

看了上面的文字,你可能会觉得我是为了写博文而写博文,其实并不是如此,我现在觉得写这类博文的目的在于梳理自己的观点,然后再进行表达出来,有的人可能会觉得为什么要纠结某一类观点?或者认为陷在一个“陷阱”中出不来,其实这只是表面如此,我的想法是通过某一类东西,去体会、学习它的过程,就像我们去某一地方旅行,你在乎到达目的地的心情吗?其实并不尽然,你应该在意的是,在这个旅行过程中,你自己有没有享受、体会或得到什么?这才是旅行的真正意义所在,我个人觉得这个过程对我非常有帮助,但如果把这个过程分享出来,不经意的一瞬间,对一部分朋友有所共鸣,那我觉得这是额外惊喜。


言归正传,上篇博文主要是通过三个问题,然后去思考实体和值对象的概念,通过实际场景去学习、理解领域模型的概念,感觉确实非常好,但第三个问题,我和 netfocus 兄在上一篇博文中探讨了好久,但遗憾的是,到最后也没准确的确定下来,这也是我写这篇博文的一部分初衷,希望可以再次通过这个“难缠”的问题,可以更深一步的理解实体和值对象。

  • 主题:消息场景中,发件人、收件人是实体?还是值对象?

发件人、收件人设计为实体会怎样?

在上一篇博文中,第一个问题是:实体的最重要特性是什么?最后归纳为两点:连续性(continuity)和标识(identity),然后在第三个问题分析中,结合发件人、收件人(以下用联系人表示)是否符合或存在这两个特性,可能在我的分析中有些牵强,所以最后我的结论是:联系人应该设计为实体。

具体实体的两个特性分析可以参考上一篇博文,消息场景中的业务非常简单,其实就存在两种“东西“:消息和联系人,当然还有一些其他的,但都不是主要的,他们俩才是主角,这两个东西设计的稍微不同,最后实现起来可能就会千差万别。但首先明确一点的是,在消息场景中,联系人是依附于消息的,脱离于消息,联系人将毫无意义,毕竟这是消息场景,而不是人员管理场景,也可以这样说:消息是男一号,那联系人是男二号,并且男二号没有“上位”的可能。

在其他的业务场景中,你会发现这种“依附”关系非常普遍,也可以说是一个应用场景最基本的关系,比如购物车场景中的 Order 和 Customer 等等,在特定的场景中,依附关系是确定的,但换一种场景,这两者之间的关系可能就会“逆向”过来,那针对这种最普遍的关系该怎么进行设计呢?

在上篇评论中我有提到,《领域驱动设计》书中第5.3.1章设计值对象,作者列出了这样一个关联设计的例子:

在电力运营公司的软件中,一个地址对应于公司线路和服务的目的地。如果多个住所都申请了电力服务,那么这个公司需要知道这一点,因此地址是实体。我们也可以用另一种方法,在模型中将“住所”关联到运营服务,其中“住所”是一个包含地址属性的实体。此时,地址就是一个值对象。

虽然很简短的一段话,但信息量太大了,我觉得理解了这段话对如何设计实体和值对象非常有帮助,我们看一下后面这段话:“住所”关联到运营服务(注意场景是电力运营),是不是有点像联系人关联消息呢,在电力运营场景中。“住所”的概念脱离运营服务也将毫无意义,再到后面:其中“住所”是一个包含地址属性的实体,是不是又有点像联系人包含名称以及其他属性的实体,它最后说的“此时,地址就是一个值对象”,其中的地址可以看作是联系人的某一个属性,比如联系人名称。

在另外一本 DDD 著作《实现领域驱动设计》第5章实体,作者一开始说了这样一段话:

唯一的身份标识和可变性(mutability)特性讲实体对象和值对象区分开来。

先看第一个,联系人是否存在唯一标识?这个在上一篇博文中就已经分析了,在消息场景中,联系人必须是唯一的,这个没什么可争议的,即使是另一种设计 SenderId、RecipientId,那这个值也是唯一的,这其实就是联系人的标识,后面可变性(mutability)是什么意思呢?和上一篇博文说的连续性(continuity)有什么区别?其实我个人觉得是一个意思,值对象从应用程序一开始就创建了,并在整个过程中,它是不可变的,而实体在其自己的生命周期内,是可变的,连续性指的是实体可变的连续,它是一个过程,就像一个人从出生到死亡,在其生命过程中,他必须首先确定他是哪个人,比如可以通过身份证号进行标识,然后他自己的一些特征可能会发生变化,比如工作、生活等,这个可以看作是可变性的体现,但必须都是在唯一标识确定的前提下,这部分内容我自己表达的有些杂,可能不太好理解,大家意会就行了。

接上面,在消息场景中,最基本的业务用例是:用户 A 给用户 B 发一个消息,然后用户 B 给用户 A 回复一个消息。。。在这个过程中,我们用 SenderId、RecipientId 来区分是哪个联系人,发送是一个动作,但在这个基本用例中,除了发送可能还会包含一些其他的东西,比如我要对联系人进行验证,就像我们买车票一样,在买之前会有一些身份验证,来确定你的身份是否合法?那这个联系人的验证过程是消息场景中的一部分?还是用户场景的一部分?我觉得这是消息场景的一部分,因为针对用户的验证都是在发消息这个动作基础上完成的,可以理解为这不属于发消息,是独立的联系人验证,但这个必须是在消息场景下。针对联系人的设计之前可能只有 Id,但消息中要进行联系人显示啊,所以后来加了 Name,再后来又要对联系人进行验证,所以又加了 IsGagged。。。这是一个不断完善的过程,这时候你会发现,在联系人对象中,除了标识之外,其他一些属性都是可能会变化的,也就是《实现领域驱动设计》书中所提到的可变性。

以上扯的有点“云里雾里”的感觉,回到这个标题上,联系人设计成实体会怎样?首先看一下 Message 消息实体中的部分代码:

public virtual Contact Sender { get; set; }
public virtual Contact Recipient { get; set; }

Message 实体中有类型为 Contact 的 Sender、Recipient 对象,用来标识此消息的发件人和收件人,这一点没什么问题,虽在实体中为对象关联,但在数据库的体现可能是 SenderId 和 RecipientId,这不是我们关心的,我们只需要操作模型中的对象即可,至于 Contact 中实体的具体设计,可以根据具体的消息场景进行设计,比如最简单的示例代码:

public class Contact
{
public int ID { get; set; }
public string DisplayName { get; set; }
public bool IsGagged { get; set; }
}

联系人设计为实体,首先符合实体的一些特性,并且在消息场景中,可以更好的对联系人进行验证,联系人存储虽不在消息中进行存储,但消息缺少联系人同样不行,所以针对消息中的联系人验证还是很有必要的,还有就是,如果哪一天消息中联系人要单独进行管理了,这时候首先确定的是联系人肯定为实体,另一个重要需要考虑的是,消息和联系人聚合问题,就像购物车中的 Orde 和 Custorm 一样。

发件人、收件人设计为值对象会怎样?

首先,现在的消息模型就是把联系人设计为值对象,具体是怎么设计的,我再详细描述下,Contact 的设计就类似上面的代码,只不过命名空间为:CNBlogs.Msg.Domain.ValueObject,然后 Contact 和 Message 的关联也像上面如此,只不过在存储的时候,需要把 Contact 中所有属性映射到 Message 中,而不只是上面的 SenderId 和 RecipientId,为什么?因为既然联系人为值对象,那其中所有属性值必须唯一,一个值不同或发生变化,那就是一个全新的值对象,而且值对象中的某一个属性代表不了整个值对象。

针对上面的问题,我举一个例子进行说明,比如 NBA 球队之间打比赛,都是五个人之间的对抗,某一个人代表不了整个球队,即使他再牛叉,而且一个完整的球队,如果某一个人离开了,那这个球队就会发生变化,对手就会根据球队的变化做出相应的调整,这个例子可能说的有些牵强,意会就行了。

按照上面设计就会造成一个结果,如果消息场景中联系人的信息比较简单,是可以了,但如果比较复杂,然后这些属性都必须体现在 Message 中,这样就会造成 Message 实体变的非常冗余,有人看到这,可能会说,为什么要在 Message 实体中去关联 Contact 对象,直接用 SenderId 和 RecipientId 表示不行吗?比如 Message 实体中的部分代码:

public int SenderId { get; set; }
public int RecipientId { get; set; }

这样设计我觉得没什么不可以,更加简化了消息场景中的复杂度,直接一个 Message 实体就可以了,操作或存储起来也很方便,比如我要获取一个消息进行展示,这个操作可能会在仓储中进行完成的,获取 Message 对象后,还要在应用层进行“组装”DTO,因为消息联系人展示肯定要用名称,而不是标识 Id,听起来似乎很合理。但上面曾说过的联系人验证,这个该怎么实现呢?关于这个实现,现在的操作是放在应用层中,因为没有联系人对象的说法,它现在表现出来的只是一个 Id 值,而且发送是根据显示名称发送的,在发送消息操作中,先根据名称获取 Id,然后再根据 Id 获取 IsGagged,然后才是发送操作,这部分的实现现在和领域没有半毛钱关系,那它是什么?应用层控制的是工作流程,但显然这部分工作并不是工作流程,它应该是消息场景中业务的一部分。

还有就是,这部分设计最直白的问题是,首先看上去就有点“不合理”,难道以后应用中对象之间的关联都必须使用 Id?那这样的话,也就没有了“对象关联”的概念存在了。

再次回到标题上来,联系人设计为值对象会怎么?在现有的场景中,我觉得没有什么问题,但针对联系人的验证或管理变的复杂的话,这时候就要考虑下,联系人设计为值对象是否合理,因为现有针对这部分的实现都不是在领域中完成的,为什么不放在领域中?因为现有设计中,联系人没有对象的概念,它只是一个值,一个具体的值。所以在领域模型演化的过程中,针对不断变化的业务场景,根据现有的设计,还需要考虑模型的合理性。

之前列出了实体的两个特征,下面列一下值对象的几个特征,来自《实现领域驱动设计》第六章值对象:

  • 它度量或者描述了领域中的一件东西。
  • 它可以作为不变量。
  • 它将不同的相关的属性组合成一个概念整体(Conceptual Whole)。
  • 当度量和描述改变时,可以用另一个值对象予以替换。
  • 它可以和其它值对象进行相等性比较。
  • 它不会对协作对象造成副作用。

当在应用设计过程中,如果不能准确的区分实体和值对象,可以不妨把应用程序所抽离出来的对象,往实体和值对象的几个特征上面套,看看哪一个是否更加合理,但设计不是绝对的,一种思想就会导致一种设计,思想的稍微不同,最后的设计可能就会千差万别。

其实写到这,就会发现这篇博文的主题并不只是来确定:消息场景中,发件人、收件人是实体?还是值对象?只不过通过这个问题,可以去发现实体和值对象的一些不常遇到的地方,这些东西在以后的设计中可能会有所帮助。当然对于这个问题,你问我是设计为实体?还是值对象?我个人还是比较偏向于实体,嘿嘿。

通过问题探讨去学习领域驱动设计,这种方式会一直持续下去,这篇博文就写到这!

DDD 领域驱动设计-三个问题思考实体和值对象(续)的更多相关文章

  1. DDD 领域驱动设计-“臆想”中的实体和值对象

    其他博文: DDD 领域驱动设计-三个问题思考实体和值对象 DDD 领域驱动设计-三个问题思考实体和值对象(续) 以下内容属于博主"臆想",如有不当,请别当真. 扯淡开始: 诺兰的 ...

  2. DDD 领域驱动设计-三个问题思考实体和值对象

    消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A... 现有设计:消息设计为实体并为聚合根,发件人.收件人设计为值对象. 三个问题: 实体最重要的特性是什么? Messag ...

  3. C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)

    前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...

  4. DDD领域驱动设计初探(三):仓储Repository(下)

    前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...

  5. DDD 领域驱动设计-如何完善 Domain Model(领域模型)?

    上一篇:<DDD 领域驱动设计-如何 DDD?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 阅读目录: ...

  6. DDD 领域驱动设计-看我如何应对业务需求变化,领域模型调整?

    写在前面 上一篇:DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对? "愚蠢的应对",这个标题是我后来补充上的,博文中除了描述需求变化.愚蠢应对和一些思考,确实没有实质性 ...

  7. C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上)

    前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原 ...

  8. DDD领域驱动设计仓储Repository

    DDD领域驱动设计初探(二):仓储Repository(上) 前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repositor ...

  9. 关于DDD领域驱动设计的理论知识收集汇总

    原文:关于DDD领域驱动设计的理论知识收集汇总 最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下: 我一直觉得不要盲目相信权威,比如不能一 ...

随机推荐

  1. webScoket的浅短的认识

    在一般的发送数据请求的时候都是用的http协议,但是对于类似即时聊天,需要客户端与服务器不间断的交互的时候对于http协议来说就不太适用了.因为http协议无法主动把数据发到客户端,而且客户端发送请求 ...

  2. iOS 中 ARC 项目 兼容 MRC

    iOS 项目中MRC 和 ARC 项目的代码兼容问题: 1.ARC 项目中导入 MRC 第三方类的时候要在此类上添加 -objc-arc. 2.MRC 项目中导入 ARC 类的时候要在次类上添加 -f ...

  3. JavaScript 数组

    JavaScript 数组 简介:数组是值的有序集合,JavaScript在同一个数组中可以存放多种类型的元素,而且是长度也是可以动态调整的,可以随着数据增加或减少自动对数组长度做更改. 一:创建数组 ...

  4. MAC系统设置SSX教程与下载

    http://ss.hongxingchajian.com MAC系统设置SSX教程与下载 1.下载客户端并安装,装完后打开 链接: http://pan.baidu.com/s/1o7ypp5g 密 ...

  5. 深入理解css BFC 模型

    如果要深入理解css布局的各种原理,要在重构页面做得心应手的话,那么你就必须先理解这玩意 "BFC" , BlockFormatting Context(块级格式化上下文): 这里 ...

  6. SQL Server 触发器

    触发器是一种特殊类型的存储过程,它不同于之前的我们介绍的存储过程.触发器主要是通过事件进行触发被自动调用执行的.而存储过程可以通过存储过程的名称被调用. Ø 什么是触发器 触发器对表进行插入.更新.删 ...

  7. [LeetCode] All solution

    比较全的leetcode答案集合: kamyu104/LeetCode grandyang

  8. 开始研究unreal4了

    最后一个周末了,昨天去做了许多事,算是对最近的一些整理和了结吧.早上广州下雨了,9点起来吃了早餐之后又睡了1个小时.中午吃了泡面,幸福感max.晚上煎了菜脯蛋和肉卷,拖着拉着把<旋风十一人> ...

  9. 用jdbc访问大段文本数据

    package it.cast.jdbc; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.F ...

  10. matlab size、numel、length、fix函数的使用,补充nargin

    size():获取矩阵的行数和列数 (1)s=size(A), 当只有一个输出参数时,返回一个行向量,该行向量的第一个元素时矩阵的行数,第二个元素是矩阵的列数.(2)[r,c]=size(A), 当有 ...