上一篇: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领域驱动设计三、用事件风暴构建领域模型

    文章目录 一.准备事件风暴 1.参与人员 2.环境条件 二.确定产品愿景 参与角色: 三.业务场景分析 1.参与角色: 2.实例 四.领域建模 1.参与角色: 2.思考 3.实例 五.微服务拆分与设计 ...

  2. DDD领域驱动设计-视频讲解+实战

    目录 简介 解决的问题 过度耦合 现状 DDD的分层架构和构成要素 小结 分包应用 DDD领域驱动设计:实体.值对象.聚合根 DDD应用 战略建模 领域 限界上下文 需求分析 上下文映射图 战术建模- ...

  3. DDD - 一文读懂DDD领域驱动设计

    一文读懂DDD领域驱动设计 1. 领域驱动设计简介 1.1 什么是领域驱动设计 1.2 为什么要用领域驱动设计 优点 缺点 2.3 领域驱动设计过程 2. 对于DDD,我们需要学习什么? 2.1 DD ...

  4. DDD 领域驱动设计

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  5. 浅谈我对DDD领域驱动设计的理解

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  6. C#进阶系列——DDD领域驱动设计初探(五):AutoMapper使用

    前言:前篇搭建了下WCF的代码,就提到了DTO的概念,对于为什么要有这么一个DTO的对象,上章可能对于这点不太详尽,在此不厌其烦再来提提它的作用: 从安全上面考虑,领域Model都带有领域业务,让Cl ...

  7. [转]浅析DDD(领域驱动设计)

    最近在做一些微服务相关的设计,内容包括服务的划分,Restful API的设计等.其中比较棘手的就是Service的职责划分:如何抽象具有统一业务范畴的Model,使其模块化,又如何高度提炼并组合多模 ...

  8. 浅析DDD(领域驱动设计)

    最近在做一些微服务相关的设计,内容包括服务的划分,Restful API的设计等.其中比较棘手的就是Service的职责划分:如何抽象具有统一业务范畴的Model,使其模块化,又如何高度提炼并组合多模 ...

  9. DDD 领域驱动设计落地实践:六步拆解 DDD

    引言 相信通过前面几篇文章的介绍,大家对于 DDD 的相关理论以及实践的套路有了一定的理解,但是理解 DDD 理论和实践手段是一回事,能不能把这些理论知识实际应用到我们实际工作中又是另外一回事,因此本 ...

最新文章

  1. linux网卡设置adsl上网,Linux下设置ADSL自动拨号上网
  2. CSS实现英文或拼音单词首字母大写
  3. python实现WordCount(第三次作业)
  4. 令人郁闷的discuz!个人空间过滤机制
  5. Python机器学习笔记:深入理解Keras中序贯模型和函数模型
  6. SQLServer 大小写敏感配置
  7. 前端学习(1381):多人管理项目1项目管理搭建
  8. 阿里P8架构师谈:MongoDB、Hbase、Redis等NoSQL优劣势、应用场景
  9. Visual Studio 与 Eclipse,谁是最强 IDE?
  10. 关于页面 reflow 和 repaint
  11. Memory for crash kernel (0x0 to 0x0) notwithin permissible range
  12. 极客大学产品经理训练营 产品思维和产品意识 解决方案的设计与积累 作业3
  13. 完全卸载mysql数据库
  14. 关于 Mythware 极域电子教室
  15. 【第3版emWin教程】第14章 emWin6.x的2D图形库之基本绘图
  16. oracle数据库报01033,oracle数据库报ORA-01033错误
  17. java 详细教程AXIS调用webservice(直接上代码)
  18. [AWCC]DELL WINDOWS10 无法打开这个应用 请去windows 应用商店查看有关alienware command centor的更多信息 解决办法
  19. 使用Fireworks 8制作网页效果图
  20. SSH CA User Key实验

热门文章

  1. php调用restful接口_如何使用PHP编写RESTful接口
  2. tar -zxvf命令_Linux压缩命令小记
  3. matlab ofdmmodulator,那位高手指点一下OFDM的基本仿真,用MATLAB,谢谢了
  4. mysql8.0.19初始密码输入错误_MySQL 8.0.19支持输入3次错误密码锁定账户功能(例子)...
  5. SpringBoot POM说明
  6. hadoop 配置 docker伪分布式(单节点)
  7. Linux echo
  8. opencv-api pyrDown
  9. python r语言 数据分析_PythonR语言-将Python和R整合进一个数据分析流程
  10. 中国数据中心行业深度分析