基于事件驱动的领域模型实现框架 - 分析框架如何解决各种典型业务逻辑场景

  1. 任何一个领域对象是“活”的,它不仅有属性(对象的状态),而且有方法(对象的行为)。为什么说是“活”的呢?因为领域对象的行为都不是被另外的领域对象调用的,而是自己去响应一些“事件” ,然后执行其自身的某个行为的。在我看来,如果一个领域对象的方法是被其他的领域对象调用的,那这个对象就是“死”的,因为它没有主动地去参与到某个活动中去。这里需要强调的一点是,领域对象只会更新它自己的状态,而不会更新其他领域对象的状态。
  2. 所有的领域对象之间都是平等的,任何两个领域对象之间不会有任何引用的关系(如,依赖、关联、聚合、组合);但是它们之间会存在数据上的关系,如一个对象会保留另外一个对象的唯一标识。
  3. 领域对象之间的交互和通信全部通过事件来完成,事件可以将所有的领域对象串联起来使它们能相互协作。除此之外,领域对象和外界的各种交互也通过事件完成。按照Eric Evans的理论,为了确保领域对象之间的概念完整性,需要有聚合及聚合根的概念,聚合根聚合了很多子的实体或值对象,或者还会关联其他的聚合根。另外,每个聚合需要有一个仓储(Repository)来负责聚合的持久化和重建的职责。其实,我觉得要确保领域对象之间的概念完整性,除了通过聚合的方式之外,还可以通过事件来确保。其实,用聚合来确保概念完整性是事物之间直接作用的反映;而用事件来确保概念完整性则是事物之间间接作用的反映。在用事件的方式下,仓储也不再需要了,因为领域模型和外界的交互也是通过事件来完成的。 

虽然在前面那篇文章中提供了两个Demo用来展示框架的功能,但我想大家直接看Demo源代码还是比较累,并且不能直观的看到框架能做什么以及如何使用。因此,本篇文章打算举几个典型的例子来分析如何使用我的框架来解决各种典型的应用场景。

应用场景1:银行转账

银行转账的核心流程大家应该都很熟悉了,主要有这么几步:

  1. 源账户扣除转账金额,当然首先需要先判断当前账户余额是否足够,如果不够,则无法转账。
  2. 目标账户增加转账金额;
  3. 为源账户生成一笔转账记录;
  4. 为目标账户生成一笔转账记录;

下面看看如何通过事件来实现上面的应用场景:

首先定义一个转账的事件:

1     public class TransferEvent : DomainEvent
2     {
3         public Guid FromBankAccountId { get; set; }
4         public Guid ToBankAccountId { get; set; }
5         public double MoneyAmount { get; set; }
6         public DateTime TransferDate { get; set; }
7     }

该事件定义了:源账户、目标账户、转账金额、转账时间四个信息;

然后看看银行帐号类的设计:

 1     public class BankAccount : DomainObject<Guid>
 2     {
 3         #region Constructors
 4 
 5         public BankAccount(Guid customerId) : base(Guid.NewGuid())
 6         {
 7             this.CustomerId = customerId;
 8         }
 9 
10         #endregion
11 
12         #region Public Properties
13 
14         public Guid CustomerId { get; private set; }
15         [TrackingProperty]
16         public double MoneyAmount { get; set; }
17 
18         #endregion
19 
20         #region Event Handlers
21 
22         private void TransferTo(TransferEvent evnt)
23         {
24             WithdrawMoney(evnt.MoneyAmount);
25 
26             CreateTransferHistory(evnt.FromBankAccountId,
27                                   evnt.FromBankAccountId,
28                                   evnt.ToBankAccountId,
29                                   evnt.MoneyAmount,
30                                   evnt.TransferDate);
31         }
32         private void TransferFrom(TransferEvent evnt)
33         {
34             DepositMoney(evnt.MoneyAmount);
35 
36             CreateTransferHistory(evnt.ToBankAccountId,
37                                   evnt.FromBankAccountId,
38                                   evnt.ToBankAccountId,
39                                   evnt.MoneyAmount,
40                                   evnt.TransferDate);
41         }
42 
43         #endregion
44 
45         #region Private Methods
46 
47         private void WithdrawMoney(double moneyAmount)
48         {
49             if (this.MoneyAmount < moneyAmount)
50             {
51                 throw new InvalidOperationException("账户余额不足。");
52             }
53             this.MoneyAmount -= moneyAmount;
54         }
55         private void DepositMoney(double moneyAmount)
56         {
57             this.MoneyAmount += moneyAmount;
58         }
59         private void CreateTransferHistory(Guid currentBankAccount,
60                                            Guid fromBankAccountId,
61                                            Guid toBankAccountId,
62                                            double moneyAmount,
63                                            DateTime transferDate)
64         {
65             TransferHistory transferHistory =
66                 new TransferHistory(
67                     fromBankAccountId,
68                     toBankAccountId,
69                     moneyAmount,
70                     transferDate);
71 
72             Repository.Add(transferHistory);
73 
74             EventProcesser.ProcessEvent(
75                 new AddAccountTransferHistoryEvent
76                 {
77                     BankAccountId = currentBankAccount,
78                     TransferHistory = transferHistory
79                 });
80         }
81 
82         #endregion
83     }

BankAccount是一个领域对象,TransferTo和TransferFrom是两个事件的响应函数。目前为止,我们只需要知道:1)TransferTo方法会自动被源帐号对象调用,2)TransferFrom方法会自动被目标帐号对象调用。

最后,如何来通知领域模型进行转账操作呢?很简单,只要触发TransferEvent事件即可:

1     EventProcesser.ProcessEvent(
2         new TransferEvent {
3             FromBankAccountId = bankAccount1.Id,
4             ToBankAccountId = bankAccount2.Id,
5             MoneyAmount = 1000,
6             TransferDate = DateTime.Now
7         }
8     );

上面的代码通知中央事件处理器处理一个转账的事件。 

好了,理想情况下,如果只要上面的这样三段代码就能完成转账的业务场景了,那就太好了。但是那时不可能的,因为还有一个很重要的信息没有告诉框架,那就是框架还不知道源账号和目标账号的唯一标识,我们需要告诉框架源账号的唯一标识是从事件的那个属性中获取,目标帐号的唯一标识是从事件的那个属性中获取。如下的代码体现了这点:

 1     RegisterObjectEventMappingItem<TransferEvent, BankAccount>(
 2         new GetDomainObjectIdEventHandlerInfo<TransferEvent>
 3         {
 4             GetDomainObjectId = evnt => evnt.FromBankAccountId,
 5             EventHandlerName = "TransferTo"
 6         },
 7         new GetDomainObjectIdEventHandlerInfo<TransferEvent>
 8         {
 9             GetDomainObjectId = evnt => evnt.ToBankAccountId,
10             EventHandlerName = "TransferFrom"
11         }
12     );

上面的代码表示,一个BankAccount对象会响应TransferEvent事件,并且会有两个方法会响应;“TransferTo”方法表示源账号对TransferEvent事件的响应,“TransferFrom”方法表示目标帐号对TransferEvent事件的响应;另外,通过传递给框架一个“GetDomainObjectId”委托函数来告诉框架,当前响应者的唯一标识。通过上面的四段代码,我们就能实现转账的应用场景了。可以看出,转账的逻辑都在BankAccount对象中,而RegisterObjectEventMappingItem方法则是用来告诉框架BankAccount对象的唯一标识是从TransferEvent事件中的那个属性中获取的。另外一般情况下,我们不需要指定事件响应函数的名字,但由于这里一个对象对同一个事件有两个响应函数,则需要额外指定一个名字来告诉框架对应关系。

应用场景2:论坛中帖子的回复对帖子的影响

大家都知道,一个论坛的注册用户可以发表帖子,发表帖子的回复,或者是删除自己发表的某个回复。假设有如下的场景:帖子有一个属性表示它有多少个回复,当该帖子新增一个回复时,该属性值加1;当该帖子删除一个回复时,该属性值减1。

首先看一下帖子类:

 1     public class Topic : DomainObject<Guid>
 2     {
 3         #region Constructors
 4 
 5         public Topic(Guid createdBy, DateTime createDate, int totalReplyCount) : base(Guid.NewGuid())
 6         {
 7             this.CreatedBy = createdBy;
 8             this.CreateDate = createDate;
 9             this.TotalReplyCount = totalReplyCount;
10         }
11 
12         #endregion
13 
14         #region Public Properties
15 
16         public Guid CreatedBy { get; private set; }         //作者
17         public DateTime CreateDate { get; private set; }    //创建日期
18         [TrackingProperty]
19         public string Subject { get; set; }                 //标题
20         [TrackingProperty]
21         public string Body { get; set; }                    //消息内容
22         [TrackingProperty]
23         public int TotalMarks { get; set; }                 //点数
24         [TrackingProperty]
25         public int TotalReplyCount { get; set; }            //当前主题下的消息总数
26 
27         #endregion
28 
29         #region Event Handlers
30 
31         private void Handle(DomainObjectAddedEvent<Reply> evnt)
32         {
33             this.TotalReplyCount += 1;
34         }
35         private void Handle(DomainObjectRemovedEvent<Reply> evnt)
36         {
37             this.TotalReplyCount -= 1;
38         }
39 
40         #endregion
41     }

Topic类表示一个帖子,大家可以看到它响应两个事件DomainObjectAddedEvent<Reply>和DomainObjectRemovedEvent<Reply>,其中Reply类表示帖子的回复。

DomainObjectAddedEvent<TDomainObject>和DomainObjectRemovedEvent<TDomainObject>这两个事件是由框架定义的泛型事件,用来表示某个领域对象被创建了或被移除了。所以,DomainObjectAddedEvent<Reply>和DomainObjectRemovedEvent<Reply>这两个事件就表示新增了一个帖子的回复的事件和移除了一个帖子的回复的事件。

另外,我们可以通过下面的代码来添加一个回复,或移除一个回复。

1 var reply1 = Repository.Add(new Reply(topic.Id) { Body = "A new topic reply1." }); //添加回复的代码
2 Repository.Remove(reply1); //移除回复的代码 

大家从上面的代码中看到了Repository,也就是仓储。其实这个类不是真正的仓储,因为它的内部实现也仅仅是做了“发布事件”的事情。换句话说,我这里的Repository只是帮我们做了发布一些通用典型事件的操作。可以看一下这两个方法的实现:

 1         public static TDomainObject Add<TDomainObject>(TDomainObject domainObject) where TDomainObject : class
 2         {
 3             EventProcesser.ProcessEvent(new PreAddDomainObjectEvent<TDomainObject> { DomainObject = domainObject });
 4             EventProcesser.ProcessEvent(new AddDomainObjectEvent<TDomainObject> { DomainObject = domainObject });
 5             EventProcesser.ProcessEvent(new DomainObjectAddedEvent<TDomainObject> { DomainObject = domainObject });
 6             return domainObject;
 7         }
 8         public static void Remove<TDomainObject>(TDomainObject domainObject) where TDomainObject : class
 9         {
10             EventProcesser.ProcessEvent(new PreRemoveDomainObjectEvent<TDomainObject> { DomainObject = domainObject });
11             EventProcesser.ProcessEvent(new RemoveDomainObjectEvent<TDomainObject> { DomainObject = domainObject });
12             EventProcesser.ProcessEvent(new DomainObjectRemovedEvent<TDomainObject> { DomainObject = domainObject });
13         }

当然,和前面的例子一样,我们还必须告诉框架,从事件的那个部分去获取事先响应者的唯一标识,我们可以通过下面的简单明了的代码来告诉框架这个信息:

1         RegisterObjectEventMappingItem<DomainObjectAddedEvent<Reply>, Topic>(evnt => evnt.DomainObject.TopicId);
2         RegisterObjectEventMappingItem<DomainObjectRemovedEvent<Reply>, Topic>(evnt => evnt.DomainObject.TopicId);

我们可以看出,这里的代码要币上例的代码简单很多,原因是Topic类对同一个事件的响应函数只有一个。这里,我们仅仅只是提供了一个委托用来告诉框架Topic的唯一标识如何获取,这样就足够了。其实在大多数情况下,一个类对某个事件的响应函数只有一个,也就是说,只要确定了领域对象类型和事件类型,我们就可以找到对应的响应函数了。

应用场景3:论坛中帖子被删除后帖子回复的级联删除

本篇文章一开始简单讨论了聚合和仓储的概念。首先聚合有业务逻辑,而仓储是用来持久化整个聚合的,那么仓储也肯定知道它所管理的聚合的业务逻辑。也就是说,仓储在持久化聚合时,肯定知道了该聚合内的哪些对象需要被一起持久化,哪些则不用。比如下面的例子:

Book.Author

Book.Comments

假设有一本书,用Book表示;它是一个聚合根,一本书有一些评论,用Comments表示书本的所有评论,书本评论离开书本没有意义,类似于Order和OrderItem之间的关系,所以Book聚合了一些Comments;另外,一本书有一个作者,用Author表示。一般情况下,Author也是一个聚合根,因为它是独立于书本而存在的。当我们删除一本书时,书本的作者肯定不能被删除,最多删除他们之间的关系。好了,有了上面这些前提条件后,假设有一个BookRepository,它负责持久化Book。则BookRepository的RemoveBook方法看起来应该是这样:

bookRepository.RemoveBook(book)

{

    //delete book it self;

    //delete book comments;

    //remove the relationship between book and author; 

}

我们可以充分看到上面的方法之所以知道当一本书需要被删除时需要做哪些事情,是因为BookRepository完全知道整个聚合(这里就是Book)的所有和聚合相关的业务逻辑。事实上,在Eric Evans的DDD理论中,也正是通过聚合及仓储的设计来确保各个领域对象之间的概念完整性的。

但是,我上面提到过,没有了聚合,没有了仓储,我们还可以通过事件来确保领域对象的完整性。下面举个例子来说明如何实现这个目标:

大家都知道一个论坛中帖子与帖子回复的关系应该是和书本与书本评论的关系是同一种关系。也就是说,当我们在删除一个帖子时,还需要级联删除帖子的回复。

帖子类的实现上面已经写了,这里我们看一下帖子回复类的实现:

 1     public class Reply : DomainObject<Guid>
 2     {
 3         #region Constructors
 4 
 5         public Reply(Guid topicId) : base(Guid.NewGuid())
 6         {
 7             this.TopicId = topicId;
 8         }
 9 
10         #endregion
11 
12         #region Public Properties
13 
14         public Guid TopicId { get; private set; }     //主题ID
15         [TrackingProperty]
16         public string Body { get; set; }              //消息内容
17 
18         #endregion
19 
20         #region Event Handlers
21 
22         private void Handle(DomainObjectRemovedEvent<Topic> evnt)
23         {
24             Repository.Remove(this);
25         }
26 
27         #endregion
28     }

可以看到回复类有一个事件响应函数,该函数表示当其所属的帖子删除时,需要把自己也一起删除。也就是说,当我们在执行如下代码时,上面代码中的响应函数就会自动被执行。

1 Repository.Remove(topic);

当然,框架还不可能也做不到这么智能的地步。我们必须告诉框架哪些回复回去响应DomainObjectRemovedEvent<Topic>事件。如下代码所示:

    RegisterObjectEventMappingItem<DomainObjectRemovedEvent<Topic>, Reply>(
        evnt => Repository.Find<Reply, FindTopicRepliesEvent>(evt => evt.TopicId = evnt.DomainObject.Id));

上面的代码表示帖子回复类会去响应DomainObjectRemovedEvent<Topic>事件,也就是帖子被删除的事件,并且通过一个委托来告诉框架有哪些回复会响应该事件。

总结: 

从上面的几个例子,我们可以清楚的看到领域对象之间没有相互引用,完全通过事件来实现相互协作。比如父对象通知子对象,子对象通知父对象,一个事件通知一个或多个同类型或不同类型的对象,等等。要实现任何一个业务场景,我们需要做的事情一般是:

1)通知中央事件处理器处理某个事件(如果该事件是框架没有提供特定的业务事件,则需要自己定义,如TransferEvent);

2)领域对象响应该事件,通过定义私有的响应函数来实现响应;

3)在领域模型内部,告诉框架事件及响应者之间的映射关系,并告诉框架有哪个或哪些对象会去响应,它们的唯一标识是如何从事件中获取的;

通过这三个步骤,我们就可以轻松简单的实现各种领域对象之间的协作了。而且需要强调的是,通过这样的方式,可以充分体现出领域对象是“活”的这个概念。因为所有的领域对象的事件响应函数都是私有的,也就是领域对象自己的行为别的领域对象无法去调用,而都是由一个“中央事件处理器”去统一调用。这样的效果就是,任何一个领域对象都会“主动”去响应某个事件,这样就从分体现出了“活”对象的概念了。在我看来,这才是真正的面向对象编程,因为所有的对象都是主动参与到某个业务场景中去的。

最后,关于使用这种方式来组织业务逻辑的好处和坏处,我目前还没有仔细研究过,我还没有利用该框架做过一个真实的项目。但我想有一点是可以肯定的,那就是这应该是另外一种全新的组织业务逻辑的方法,并且它的最大特点是高度可扩展性,因为是基于事件消息机制的,把领域对象之间的耦合度降到了最低,但同时我想在可维护性方面可能会有一些缺点。 

时间: 2024-09-17 01:46:41

基于事件驱动的领域模型实现框架 - 分析框架如何解决各种典型业务逻辑场景的相关文章

ASP.NET MVC+EF框架+EasyUI实现权限管理系列(4)-业务逻辑层的封装

原文:ASP.NET MVC+EF框架+EasyUI实现权限管理系列(4)-业务逻辑层的封装 ASP.NET MVC+EF框架+EasyUI实现权限管系列 (开篇)   (1):框架搭建    (2):数据库访问层的设计Demo    (3):面向接口编程 前言:前面几篇博客我们基本已经介绍完了搭建整个项目和数据库访问层以及一些业务逻辑层的实现,当然了,我们的数据库访问层这样还是可以在进行封装的,但是我到这里就行了吧,项目也不大,不需要那么麻烦的,那么我们今天开始介绍我们需要介绍的内容,那就是我

基于事件驱动的DDD领域驱动设计框架分享(附源代码)

补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! 从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven design:领域驱动设计:软件核心复杂性应对之道>,以及另外一本Martin Flower的<企业应用架构模式>,学习到了不少关于如何组织业务逻辑方面的知识.

基于WPF或WinForm的应用程序框架

问题描述 最近想做个基于desktop模式的应用程序,因为有很多曲线.图标等复杂的分析功能,传统的b-s模式不太方便.以前一直是在用java的,现在想用java做后台(负责所有复杂计算以及数据库连击和处理),前端用c#,想用WPF或者WinForm来做.想请教一下这里的大侠,什么样的应用程序框架比较适用?我看了DevExpress的xaf,看上去不错,不知道有没有比这个更好用的.谢谢! 解决方案 解决方案二:该回复于2012-02-22 09:52:36被版主删除解决方案三:该回复于2012-0

VirtinSpector:一种基于UEFI的虚拟机动态安全度量框架设计与实现

VirtinSpector:一种基于UEFI的虚拟机动态安全度量框架设计与实现 严飞 石翔 李志华 王鹃 张焕国 通过可信硬件能够弥补单纯软件安全的不足,从整体上提高云系统的安全性.但是,面对云环境运行时的安全,传统可信硬件技术无法提供足够的保障.为此,提出了一种基于UEFI的虚拟机动态安全框架--VirtinSpector.该框架能够将UEFI固件作为可信基础,对云系统的基础设施层进行实时.动态的安全度量,提供传统可信技术无法达到的动态保护.在此框架基础上,以某国产服务器为实验平台,构建云环境

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

很久没有写博客了,一些读者也经常问问一些问题,不过最近我确实也很忙,除了处理日常工作外,平常主要的时间也花在了继续研究微软的实体框架(EntityFramework)方面了.这个实体框架加入了很多特性(例如LINQ等),目前也已经应用的比较成熟了,之所以一直没有整理成一个符合自己开发模式的实体框架,是因为这个框架和原来我的基于EnterpriseLibrary的模式还是有很大的不同,不过实体框架推出来也很久了,目前也去到了EntityFramework6了,听说7也快出来了. 随着我自己参考阅读

基于cordova如何构建跨平台的nulltouch框架

早有想法实现一个基于cordova跨平台框架. 妄称框架,当然要有一套较完善的构建思路和一个比较清晰的设计目标. 设计目标: 使用纯前端代码(html+css+js)跨平台编译成原生应用.不过,目标是需要逐步实现的,所以这里设定一个大致的构建思路: 1.前端控件的实现(布局 样式等),使用成熟的前端组件(bootstrap jquery zepto等),中间层的js封装,最终产出的结果应该是一个网站形式的前端文件(*.html,*.css,*.js). 2.native代码的开发和cordova

SSH框架总结(框架分析+环境搭建+实例源码下载)(转)

首先,SSH不是一个框架,而是多个框架(struts+spring+hibernate)的集成,是目前较流行的一种Web应用程序开源集成框架,用于构建灵活.易于扩展的多层Web应用程序.   集成SSH框架的系统从职责上分为四层:表示层.业务逻辑层.数据持久层和域模块层(实体层).   Struts作为系统的整体基础架构,负责MVC的分离,在Struts框架的模型部分,控制业务跳转,利用Hibernate框架对持久层提供支持.Spring一方面作为一个轻量级的IoC容器,负责查找.定位.创建和管

Alsa音频子系统Codec---al5623.c内核代码框架分析

驱动代码位于: sound/soc/codec/alc5623.c 随便找个linux内核都会有. 1.首先进行i2c总线驱动加载在: static int __init alc5623_modinit(void)在该函数中: i2c_add_driver(&alc5623_i2c_driver);alc5623_i2c_driver是一个结构体变量,并且已经被初始化,我们来看看它做了什么? static struct i2c_driver alc5623_i2c_driver = { .dri

基于Knex.js的Node.js ORM框架 bookshelf

bookshelf 详细介绍 一个基于Knex.js的Node.js ORM框架,支持PostgreSQL,MySQL和SQLite3 简单来说,Bookself是一个优秀的代码库,它易于阅读.理解.可扩展.它不强制你使用任何特定的校验scheme,而是提供灵活有效的关系或嵌套关系加载策略,一级类支持事务.它是一个精益的对象关系映射器(lean Object Relation Mapper),允许你使用原始的knex接口,当你需要自定义查询时,因为它有时并不能完全满足老一套的惯例. Booksh