基于数据访问的集合类型-领域驱动设计的又一种特定对象

news/2024/7/7 16:25:49

  企业级应用的一个显著特点是需要处理大量的数据,开发人员应该小心地从数据库中加载对象,确保只加载了供业务处理使用的必须的最小限度的对象集合。但真正做到这一点是很难的。其中一个原因就是:现实世界中,事物很少孤立存在,它们之间往往有着这样或是那样的联系,体现到对象模型上就是对象间的依赖。由一个对象展开,可能会依赖N个对象,N个对象又可能会依赖N*M个对象。很快,数量巨大的对象就被牵扯进来。由于在对象加载时刻根本无法预知哪些依赖的对象会被访问到,这就为最小化加载对象带来了不确定性。我们来看一个简单的例子:
  在一个论坛系统中,一个Forum(论坛)会包含多个Thread(主题),Forum与Thread之间是典型的一对多的父子关系.从领域模型的角度考虑,让Forum拥有一个Thread的集合是最直接也是最自然的实现方案.但是考虑到Thread的数据可能非常庞大,我们不可能在加载Forum时把它所有的Thread全部加载出来,而如果通过Repository进行即时查询则会导致领域对象对Repository的依赖.这在领域驱动设计中是一个非常典型的问题.目前人们通常的处理方法有两种:
1.不在意领域对象对Repository依赖,通过向领域对象注入Repository进行动态查询.
2.使用DomainEvent模式以事件机制与Repository通信获得所需的数据(http://www.udidahan.com/2009/06/14/domain-events-salvation/).
第一种方案并不值得推荐.而第二种方案虽然避免了领域对象对Repository的直接依赖,但是它的实现很不自然,"被设计"的成分过重,人们并不能从那些领域对象的业务方法中很直接地了解到为什么会触发这样或那样的事件,以及这些事件被如何处理了。因为这些领域事件本质上是应设计上的需要而建立的,并非是领域中真实发生的事件。
  让我们回到问题原点:从领域模型上看,Forum拥有一个Thread集合(这里的“集合”是一般意义上的集合,与程序无关),这是领域的一个事实。那么在实现模型的设计上,直接将Forum的Thread集合映射为Forum实体的一个集合类型(这里的“集合”特指编程语言中集合类)的字段就是一种最自然的映射,我们的问题是要控制集合内元素的加载,既要确保集合内的元素不会一次性全部加载出来,又要保证Client在访问集合内的某些元素时,它们能够被及时地加载出来。联想Repository在领域中扮演的角色和具体的实现,我们不难想到,我们的集合也可以仿照Repository来实现,这样它将满足以下两点:
1.这个集合在名义上代表着一组满足特定条件的领域对象,并且可以像普通集合类那样使用(即应提供集体类型基本API)。
2.这个集合的内部并不真的包含这些领域对象,而是在需要时从数据库中动态的加载出来。
这就是我要提出的针对上述问题的一种更自然更贴近领域的解决方案:基于数据访问的集合类型.


在通过Repository查询一个Forum包含那些thread并获取一部分子集的过程中,我们可考虑Repository和Forum,以及Forum的threads集合之间的关系:从领域模型的角度来看,Repository是一种Artifact,与领域问题无关,Forum是不需要的Repository的,而是应该有一个持有一个像List<Thread>这样的集合。但是我们显然不能这样做。那么这个时候Reppository要参与进来,帮助Forum得到它的Thread.

基于数据访问的集合类-领域驱动设计的又一种特定对象;

对于Forum的getLastPost方法,是一个值得好好考虑的问题。假如是在一个非基于DB的环境里,Forum找到最后一个发表的贴子的方法是遍历它的所有帖子,找到发表时间最晚的一个,我们认为这过程是一个“领域问题”求解的过程,也就是领域逻辑的体现。那么对于基于数据库的系统来说,由于数据量巨大,为了找到最后发表的帖子而把Forum0所有的帖子加载出来是非常愚蠢的,我们只能以Sql的形式完成查找,这样,关于找到的这部分领域逻辑就跑到Sql里去了,这看起来真得是一个很难调和的问题!至少我们现在有一种选择:即通过DDD中的

Specification之类的模式,以声明性的设计把领域逻辑保留在了业务模型中!

在一些领域对象中,总会遇到这样一些方法,他们需要从大量的对象中打到他们所需的对象,依赖它们完成业务操作,像getLastPost就是这样。也就是说:领域对象的业务方法会不可避免需要查询数据,为了保证领域对象的纯净,我们自然不会把数据查询的操作嵌入到业务方法中,我们需要更加自然或是透明的方法

事实上,这种Collection在形式上非常接近传统的集合类型,但其具体的实现是基于数据访问的.我们可以把这种集合视为一种传统集合与数据访问对象间的适配器.通过引入这种Collection,可以避免领域对象对Repository的依赖,而且领域对象依赖的只一个表征某种数据集合,并不暴露任何数据访问细节的Colection接口,这使得我们的领域对象更加"纯净",有助于将领域对象聚焦在领域逻辑上.并且,这种集合使用起来也非常地自然.

关于Collection接口的设计:
由于Collection实现是基于数据访问的,因些,有些传统集合方法并不适用于这种集合,而它也有一般集合不会有的方法.这些与它们需要访问数据库并且可能处理大量数据的特性有关.比如

与Repository的对比:
Collection与Repository有很多类似的地方.对于Repository来说,它总是有两方面的意义,从领域角度来看,Repository代表着某类领域对象的全部集合,我们可以从Repository中找到我们需要的对象,或者通过它来添加删除或更新对象,Repository在领域的实现模型(这里,我只能严谨地说是在领域的实现模型中而非领域模型)中确实是一个必要的角色.从它的具体实现来看,它实际上是一个数据访问对象(或者依赖于数据访问对象),它封装了数据访问的细节.
Collection同样具有这两方面的意义,从领域角度来看,它代表了某类领域对象的某种特定的子集,而其具体的实现也同样是对数据访问的封装.
如果说我们把ThreadRepository看成一个存储了全部Thread的集合,那么,Forum从中找到属于它的那些Thread也是可以理解的。但是这样有些意味反生了微妙的变化,即,对于Forum来说,它本身应该是一小撮Thread的集合,而这一点,在基于Repository的实现中被偷换了。

比如:我们现在需要一个对象A,但是A会依赖于对象B,那我们是否要在加载A时把B一起加载出来呢?很多时候,答案是不确定的。因为在加载A的时刻,我们没有足够信息来判断是否应该加载B,更遭的是,B有可能会依赖C,D,E...,这会使问题使变得更加复杂。一种好的解决方案就是Lazy Loading。很多ORM框架都大量采用了这种模式。但是限于



因此对于对象的加载这里有一个假定的背景:即所需对象。
,在处理对象的创建如果使用DDD,一个必须面对的问题是:如果加载数据。你不可能像在其他类型的系统中那样轻易地加载任何数据。


这样集合有点类似于一个小Repositry,或者说是Repository的子集,但更倾向于做为“集合”的形式存在,因而应该包含像size(),get(int index),remove(),add(),这样的方法。它的存在避免了领域对象对Repository的依赖。使得领域对象更加“纯”,有助于聚焦于领域问题。


基于数据访问的集合类型--一种传统集合与Repository之间适配器在DDD中应用.
如果说Repository代表某种对象的全部集合,那么 基于数据访问的集合就可以视作满足某种条件的这种对象的特定子集.

在领域驱动设计中,Repository负责聚合根的全局读写,而聚合内的其他对象则通过由聚合根开始的导航来访问.聚合为领域对象划分出一个清晰的边界.揭示了领域对象在读写一致性上的深层次关联.

聚合与Repository在理论上是完美的,但是实际应用中,开发人员会常常遇到这样一种尴尬的问题,即:当聚合根与某非聚合根对象是一对多的关系,并且非聚合根对象的数量非常庞大时,通过导航访问这些非聚合根对象可能会引发严重的性能问题.


没有针对单一Message的全局追踪与访问,所有的Message总是由它从属的Thread得到,那么这时,Thread和Message就组成了一个聚合,Thread作为聚合根,与Message之间是典型的一对多的父子关系.在基于Hibernate的实现中,它们的典型配置是:


领域驱动设计基于面向对象技术解决复杂的领域问题,数据驻留在对象内,对数据的访问体现为对象间的导航。

我对DomainEvent模式也有过研究,但是我一直认为这并不是一个完美的方案。诚然,DomainEvent使得领域对象和Application Service与Repository之间解耦,但却引入对第三方DonmainEvent的依赖,而DomainEvent在本质上与Application Service和Repository一样,并非是领域的一部分,而是应设计上的需要才建立的,人们并不能从那些领域对象的业务方法中很直接地了解到为什么会触发这样或那样的Domain Event,以及这些Domain Event被如何处理了。


http://www.niftyadmin.cn/n/4225158.html

相关文章

redis list底层数据结构

redis list数据结构 redis list数据结构底层采用压缩列表ziplist或linkedlist两种数据结构进行存储&#xff0c;首先以ziplist进行存储&#xff0c;在不满足ziplist的存储要求后转换为linkedlist列表。  当列表对象同时满足以下两个条件时&#xff0c;列表对象使用ziplist进行…

C#小结(一)

经过这段时间对C#的学习&#xff0c;对C#有了初步的了解。下面让我们一起走进C#之旅。 基本概念&#xff1a; .net/dotnet&#xff1a;一般指.Net Framework框架。一种平台&#xff0c;一种技术。 C#&#xff08;sharp&#xff09;&#xff1a;一种编程语言&#xff0c;可以…

中关村攻略

说实话&#xff0c;在伟大的祖国&#xff0c;很多事情都是很神奇的&#xff0c;能神奇到你无法想象的地步&#xff0c;比如去中关村买东西。在这个神奇的世界里&#xff0c;没有啥条款能保证你的利益&#xff0c;你被骗&#xff0c;别人只能怪你没做好功课&#xff0c;而如果你…

C#小结(二)

前言&#xff1a; 最基础的知识往往就是我们最值得重视的东西&#xff0c;它们是构成知识大厦的基石。 转义符&#xff1a; 常用的转义字符&#xff1a; \:一个字符&#xff0c;组成转义字符。一般用于表示特殊符号&#xff1b; \n:表示换行&#xff1b; \b:表示退格&…

马天宇的音乐

一个年龄相差无几的青年歌手 值的尊敬啊 最喜欢他的一首"坚强" 很有爱....

C#总结(三)

前言&#xff1a; 泛型是C#2.0的一个新增加的特性&#xff0c;是C#编程中不可缺少的部分。它是程序设计语言的一种特性&#xff0c;为使用C#语言编写面向对象程序增加了极大的效力和灵活性。 集合--ArrayList(): ArrayList():可以放各种类型的数据&#xff0c;并且不确定放多…

无耻的微软

其实满奇怪。用微软的系统&#xff0c;用微软的浏览器。用微软的输入法。我还在骂微软。 今天碰到一件非常常见的事情&#xff0c;我的移动硬盘突然了。虽然我这个人经常有备份重要数据的习惯。不过突然移动硬盘总不是个爽的事情。插了N台机器&#xff0c;统统不好使。硬盘感觉…

针对nginx应用场景的配置 知识整理

本文为转载&#xff0c;原文链接 前言 原本想写整理一篇针对nginx应用场景的相应配置&#xff0c;但发现已经有人整理了&#xff0c;而且写得非常不错&#xff0c;特意转过来 概论 Nginx 是一款面向性能设计的 HTTP 服务器&#xff0c;能反向代理 HTTP&#xff0c;HTTPS 和邮件…