领域驱动设计(DDD)- 引导大家进入我的领域模型世界之一:领域模型结构图
时间:2011-02-13 来源:netfocus
首先先把领域模型的结构图贴出来,让大家有一个直观的感觉,然后再做详细介绍。
上图就是我心目中的领域模型。该领域模型的核心是事件驱动,事件就是一个消息,是各个领域对象之间相互通信时所传递信息的载体。下面介绍一下这个模型的工作原理。
- 领域模型的组成元素:领域服务(Domain Service)+领域对象(Domain Object)+领域事件(Domain Event)+中央事件处理器(Event Processer)
- 领域服务,很薄的一层,上图中就是黑色的那个边框。它的作用有两个:1)封装整个领域模型,让领域模型的内部实现不暴露到外部;2)将领域模型的功能暴露给外部;领域服务只做一件事情,那就是发送事件给事件处理器。然后事件处理器便会将这个事件分发到不同的事件响应者,事件响应者可以是任何对象,可能是领域对象,也可能是领域模型的外部对象,如数据持久层对象。领域的边界就是服务,也是对外提供服务的唯一入口。领域服务和领域对象模型是一个业务领域的2个不同侧面。领域服务强调是从外向内看,反映 了“外部对业务领域的使用功能”;领域对象模型强调业务领域就像一个独立的具 有一定自主能力的生命体,反映了“业务领域的内部运行机制”。领域对象模型的功能是不能对外暴露的,不然会造成外部对领域对象的耦合。
- 领域对象,领域模型中的所有领域对象都相互平等,没有任何对象与对象之间的依赖。一个领域对象可能既是领域事件的发送者,也是领域事件的响应者。也就是说领域对象只关心它自己该发送哪些事件给别人或者关心自己该响应哪些事件;这里最体现我的框架特色的地方是:你完全不需要自己去把领域对象从数据持久层取出来然后注册到事件处理器,你所需要做的仅仅是在定义领域对象时告诉框架该领域对象会关心哪些领域事件即可。通过这个特性,当领域服务发送一个领域事件后,假如有三个领域对象会响应这个事件,则你不必自己去从数据持久层获取这三个领域对象,他们会被自动被框架取出来,然后调用它们的事件响应函数执行响应。
- 领域事件,它是领域模型内部通信以及领域模型和外部通信时传递的信息的载体。任何一个领域模型涉及到的业务最终都可以分解为CRUD四种操作,就像人可以被看成是碳水化合物一样。所以我设计的框架对那些原子事件进行了抽象和提取,具体的领域模型可以对框架提供的领域事件进行任意组合以赋予它们具体的业务意义。当然还会有很多的领域事件没有任何的共性,只有特定的业务逻辑才会需要,这种事件可以自己定义。大家可以参看我的源代码来具体理解。值得一提的是,我这里提到的事件不仅仅是通知别人发生了什么,而是泛指所有可能的通信情况,比如告诉别人我要什么(我想干什么),告诉别人我将要做什么,等等。有一种情况需要涉及到事件的回调机制。比如告诉别人我要什么的情况下,当发出事件后,处理事件的人需要调用事件提供的某个回调函数从而把事件所需要的信息返回给它。还有另外一种以前风云兄提供给我的,不用回调,而是让事件响应函数提供返回值,然后在事件出发完成后,取出该返回值。我认为这两种方式都可以。
- 中央事件处理器,主要负责两件事情:1)管理领域事件和事件响应者之间的映射;2)分发某个领域事件给所有事件响应者;事件与响应者之间的映射方式我目前设计了三种:1)事件的类型 和 响应者的类型 之间的映射;2)接受并分析某个事件响应者,将该响应者能响应的事件的类型和该响应者实例之间进行映射;3)接收某个Action<TEvent>委托,将该委托实例和对应的事件进行映射。对于这三种映射方式,在一个领域模型中,最常用的应该是第一种映射,因为各个领域对象之间的协作大部分在类型级别就已经可以确定下来,而不需要等到运行实例化后才能确定。
好了,上面的领域模型的分析就到此为止。现在大家可能还有很多疑问,比如:
- 根据Evans的DDD的理论,领域模型应该有聚合根,仓储等概念,但在你的领域模型中为什么没有这些概念?
- 如果所有领域对象之间的交互都由事件来完成,并且各个对象之间没有引用关系,那对象和对象之间的导航功能就没有了,这样会不会显得很不直观?
- 事件路由,即如何将事件按照指定的顺序进行传递?
- 需求一改,那领域服务也必须修改了?这样会不会导致领域服务改动比较频繁?
- 如何确保基于事件驱动的架构没有内存泄漏问题?
上面列出了5个问题,是我自己能想到的。但我想应该至少还有50000个问题大家要问,呵呵。这里我自己先回答这5个问题:
- 问题1的回答:Evans的DDD中的聚合、聚合根是为了出于一个目的而设计的,那就是现实生活中,一些物体需要被作为一个整体被一起考虑,这些物体要么一起产生,要么一起消亡,聚合的概念是为了保证领域内对象之间的一致性问题,聚合就是为了这种封装的目的而设计出来的。聚合强调外界不可以绕过聚合根直接修改子对象的状态。而 Repository则是和聚合根对应的,它的设计意图是用一个在内存中存在的集合来存储所有的同一类型的聚合根,领域模型需要某个聚合根时,就从这个集合中取,领域模型完全不需要关心该聚合根是怎么从该集合中取出来的,真正是从哪里取出来等问题。下面我说说这种思路的缺点:1)聚合根的概念虽然符合现实生活中一些物体是一个聚合,是一个整体的概念,但却同时违反了低耦合的原则,同时也阻碍了对象之间的交互。因为并不是说一个物体是某个聚合根的Child,那这个物体就不能直接和外界交互。有些情况下我们可能就会希望从Child出发来获取某个聚合根,而不是从聚合根出发来获取某个Child。比如CSDN的论坛有一个大家都知道的功能是:查询我回复过的所有的帖子。我觉得帖子是一个聚合根,回复是帖子的Child,因为回复离开帖子没有任何意义,回复必须和帖子一同消亡,也就是说帖子聚合了一些回复。如果我们现在要查询所有我回复过的帖子,那就是肯定要根据回复的作者是否是我来先找出回复,然后再找出其对应帖子,这个过程就是一个根据Child找到聚合根的例子。当然,还有很多其他例子。总之我认为“一致性”是事物相互作用的本质内在联系,也就是在一定场景下外界刺激在沿着一定路径传递而导致一系列对象的变化。所以“外界不可以绕过根实体直接修改状态”并不能反应这一本质,因为外界刺激并不全都是先作用在根对象上面的。在我看来,这种非本质的封装反而会造成耦合,尤其是采用“直接调用”的形式。应该说,“直接调用”是造成对象耦合最大根源,因为“直接调用”是在强调对象的上下级关系,这很生硬。如果我们换一种方式,用一种平等的心态去看待对象间作用关系,用“告诉我做什么”的方式而让对象间解耦,那才是正确的方式。因为聚合根没有存在的必要了,所以Repository也没有存在的必要了。
-
问题2的回答: 确实对象之间的导航功能没有了, 但是导航功能对于对象是没有意义的。对象之间关心的是如何交互,而要实现交互,可以通过导航的方式,即Model.OtherModel的方式;也可以通过Model.OtherModelId的类似于数据库外键的方式。其实还有其他的方式,但不管怎样,只要能建立对象之间的关系就能实现对象之间的协作。其实,对象之间的导航功能更多的是写出来给人看的,在OO的世界里,人类过分强调对象和对象之间的关联,从而发明了对象引用。并且这种思想在我们脑海中非常根深蒂固。而事实上大家想想,我们辛辛苦苦设计出来的对象引用,最后的目的是什么?就是为了让对象能够调用被引用对象的某个public方法以实现对象间的协作。为了达到这个目的,我们要做两件事情,1)把被引用对象查询出来(可以和主对象一起查询出来,也可以LazyLoad);2)调用被引用对象的public方法实现交互;而如果基于Model.OtherModelId的方式呢?实现对象协作的时候也是这两步:1)根据 OtherModelId查询出对应对象,然后调用对象的public方法;因此,Model.OtherModelId的方式完全与 Model.OtherModel的LazyLoad模式等效;所以,基于以上的想法,我毅然放弃了对象导航。通过事件消息的方式,我不觉得会让我们写程 序会很复杂,原因是:
1)不用考虑如何实现对象的LazyLoad;
2)如果要寻找一个事件的 相关响应对象,只要以该事件为关键字搜索整个应用程序源代码即可,比如搜 索:IEventHandler<TopicCreatedEvent>
3)在调试的模式下,效果和调试接口引用同等难度;
4)有一个非常好的优点,每个对象不用关心如何和其他对象交互,而只要关心自己能处理哪些事情即可;当你要想让对象之间交互的时候,只要把相关事件插入到某个对象中,完全不需要用任何设计模式;因为对象之间没有任何引用; 大家再想想,为什么我们需要对象级别的引用?为了查询方便?为了 进行某种交互行为?都有。那是否一定需要对象级别的引用呢?我觉得完全不需要。
原因:
1)如果你为了查询,不管是联合查询还是根据一端对另一端的查询。只要你通过主键建立了它们之间的联系,那么你总是能从数据存储那里,比如关系数据库那里 直接查询出你要的 东西而无需要求它们之间有任何对象级别的引用;
2)如果你为了交互。那么请听我下面的解释:
问自己一个问题:对象的行为究竟是什么?
大家都知道对象应该既有属性也应该有方法,也就是行为。那行为是什么呢?我觉得行为可以分两个方面去理解:
1)某个对象的固有行为;
2)某个对象对外界某种刺激的一个响应;
相比之下,我觉得我们应该经常站在后者的角度上去 思考对象的行为。也就是说,按照我的理解,对象的行为是“对某种事件(可以理解为外界某种刺激)的一个主动的响应,而不是一个仅仅用来被别人调用的死的方法。”因此,我觉得我们在为对象设计行为的时候,不仅要设计一个对象会什么,还要说明这个“会什么”是针对哪个事件的,也就是说,我们还要设计一个对象会对哪些事件(或消息,或刺激)有响应(响应就是行为),这点很重要。 - 问题3的回答:在我设计的领域模型中,事件不需要路由。原因是,各个领域对象作为事件的响应者时,都只更新它自己的状态,绝对不会去更新其它领域对象的状态,这点很重要。基于这个前提,我们可以认为各个领域对象响应事件的先后顺序不重要。事件处理器只要简单的按照响应者的注册顺序依次一个个执行即可。
- 问题4的回答:对于这个问题,我也在思考中,但我总觉得差别不会太大。按照Evans的DDD的领域模型,如果需求改了,如果会影响到领域模型,那它的领域模型也极有可能会修改,而和我的模型相比,修改的谁多谁少的问题我想要具体问题具体分析。
-
问题5的回答: 关于内存泄漏的问题,之前风云兄也提醒过我。后来我经过了思考,没有找出我的框架中有什么明显的内存泄漏的地方。我主要分析事件的响应者是否会没有机会被释放。关于我前面提到的三种映射方式,其实第一种方式是不可能有内存泄漏的问题,因为这中映射方式不是实例与实例的映射,而是Event Type和Subscriber Type之间的映射,程序运行时会根据Event Type找到对应的一些Subscriber Type,然后框架根据Subscriber Type和Event中的某个和该Subscriber Type想对应的ID从数据持久层获取对应的Subscriber,然后调用Subscriber的事件响应函数。这个过程中,由于Subscriber是数据持久层负责取出来的,比如如果我们用的是Linq to SQL,那么会从DataContext中取出来,而该Subscriber也会被DataContext自动保存和管理。而对于一个ASP.NET Web应用来说,一般是一个HttpRequest创建一个DataContext实例。当一个HttpRequest执行完成后,前一个DataContext也会自动在某个时候被GC释放掉。所以,我认为这种映射方式没有内存泄漏的问题。而对于后两种映射方式,由于是将Subscriber实例注册到事件处理器或者是直接将某个Action<TEvent>注册到事件处理器,所以需要在不需要时由用户自己负责去取消注册,而取消注册的接口我也已经在事件处理器中提供了。所以,我暂时还没有发现什么地方有严重的内存泄漏问题。
其实,说了这么多,大家可能会觉得,不就是一个Observer模式的应用吗?确实是Observer模式的一个应用,但有谁能想到该在领域模型的设计过程中全面使用基于事件驱动的领域模型设计呢?以前看过Martin Flower的企业应用架构这本书,里面提到了组织领域逻辑的一些方法,比如:事务脚本(也就是我们平时所说的凭血模型)、活动记录(用的不是很多)、Evans的DDD(充血模型)。但今天我想告诉大家,其实还有一种就是基于事件驱动的领域逻辑组织方法。
欢迎大家能对我的观点进行批评或指正。
相关阅读 更多 +