使用Apworks开发基于CQRS架构的应用程序(四):领域事件
根据wikipedia中关于“事件”的描述,“事件”可以被看成是“状态的一次变化”。例如:当一个客户购买了一台汽车,汽车的状态就从“待售”转变为“已售”。汽车销售系统则把这种状态的改变看成是一次事件的产生、发布、检测以及被更多其它应用程序所使用的过程。
对于CQRS架构的应用程序而言,事件产生于领域模型,并由领域模型发布事件同时由领域模型首次捕获并处理,因此,我们称之为领域事件(Domain Events)。在Apworks开发框架中,与领域事件相关的代码都被定义在Apworks.Events命名空间下。
请使用下面的步骤为TinyLibraryCQRS解决方案添加领域事件。
- 在 Solution Explorer 的 TinyLibraryCQRS 解决方案上,右键单击并选择 Add | New Project… 菜单,这将打开 Add New Project 对话框
- 在 Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library,确保所选的.NET Framework版本是.NET Framework 4,然后在 Name 文本框中,输入 TinyLibrary.Events,并单击OK按钮
- 在 Solution Explorer 下,右键单击TinyLibrary.Events项目的 References 节点,然后选择 Add Reference… 菜单,这将打开 Add Reference 对话框
- 在.NET选项卡下,选择Apworks,然后单击OK按钮
- 向 TinyLibrary.Events 项目添加下面的类代码,以实现领域事件
1: using System;
2: using Apworks.Events;
3:
4: namespace TinyLibrary.Events
5: {
6: [Serializable]
7: public class BookBorrowedEvent : DomainEvent
8: {
9: public long BookId { get; set; }
10: public long ReaderId { get; set; }
11: public string ReaderName { get; set; }
12: public string ReaderLoginName { get; set; }
13: public string BookTitle { get; set; }
14: public string BookPublisher { get; set; }
15: public DateTime BookPubDate { get; set; }
16: public string BookISBN { get; set; }
17: public int BookPages { get; set; }
18: public bool BookLent { get; set; }
19:
20: public DateTime RegistrationDate { get; set; }
21: public DateTime DueDate { get; set; }
22: }
23:
24: [Serializable]
25: public class BookCreatedEvent : DomainEvent
26: {
27: public string Title { get; set; }
28: public string Publisher { get; set; }
29: public DateTime PubDate { get; set; }
30: public string ISBN { get; set; }
31: public int Pages { get; set; }
32: public bool Lent { get; set; }
33: }
34:
35: [Serializable]
36: public class BookGetReturnedEvent : DomainEvent
37: {
38: public long ReaderId { get; set; }
39: public DateTime ReturnedDate { get; set; }
40: }
41:
42: [Serializable]
43: public class BookLentEvent : DomainEvent
44: {
45: public long ReaderId { get; set; }
46: public DateTime LentDate { get; set; }
47: }
48:
49: [Serializable]
50: public class BookReturnedEvent : DomainEvent
51: {
52: public long ReaderId { get; set; }
53: public long BookId { get; set; }
54: }
55:
56: [Serializable]
57: public class ReaderCreatedEvent : DomainEvent
58: {
59: public string LoginName { get; set; }
60: public string Name { get; set; }
61: }
62: }
从上面的代码我们可以看到,所有的领域事件都继承于Apworks.Events.DomainEvent类。同时,在每个领域事件上都是用了System.SerializableAttribute特性,以便系统能够序列化/反序列化这些领域事件,进而使其能够在网络上传输,或能够将其保存到存储系统中。
每当一次操作发生在我们的领域对象上(比如Book和Reader)时,这种操作有可能会改变对象的状态。例如:一个用来改变用户姓名的实例方法ChangeName会产生“用户姓名变更”的效果,于是,领域事件就由此产生了,最先被通知到该事件的就是领域对象本身,它会将这个事件记录下来,然后根据事件中所包含的数据来改变相应的状态。下面让我们看看,如何在Apworks框架的支持下,实现事件的产生和捕获。
现在让我们为之前已经定义好的Book和Reader实体添加一些业务逻辑。根据上面的分析我们不难得出,应用程序的用户可以创建读者和图书,不仅如此,读者可以从图书馆借书,也可以向图书馆还书。而对于书而言呢?它会被从图书馆借出,也会被归还回图书馆。于是,我们可以向Reader实体添加BorrowBook和ReturnBook方法,向Book实体添加LendTo和ReturnBy方法。
- 回到 TinyLibrary.Domain 项目,右键单击 References 节点,选择 Add Reference… 菜单,这将打开 Add Reference 对话框
- 在 Projects 选项卡下,选择 TinyLibrary.Events,然后单击OK按钮
- 向Book类添加下面的代码
1: public void LendTo(Reader reader)
2: {
3: this.RaiseEvent<BookLentEvent>(new BookLentEvent
4: {
5: ReaderId = reader.Id,
6: LentDate = DateTime.Now
7: });
8: }
9:
10: public void ReturnBy(Reader reader)
11: {
12: this.RaiseEvent<BookGetReturnedEvent>(new BookGetReturnedEvent
13: {
14: ReaderId = reader.Id,
15: ReturnedDate = DateTime.Now
16: });
17: }
18:
19: [Handles(typeof(BookLentEvent))]
20: private void OnBookLent(BookLentEvent evnt)
21: {
22: this.Lent = true;
23: }
24:
25: [Handles(typeof(BookGetReturnedEvent))]
26: private void OnBookReturnedBack(BookGetReturnedEvent evnt)
27: {
28: this.Lent = false;
29: }
- 向Reader类添加下面的代码
1: public void BorrowBook(Book book)
2: {
3: if (book.Lent)
4: throw new DomainException();
5: book.LendTo(this);
6: this.RaiseEvent<BookBorrowedEvent>(new BookBorrowedEvent
7: {
8: BookId = book.Id,
9: ReaderId = Id,
10: ReaderLoginName = LoginName,
11: ReaderName = Name,
12: BookISBN = book.ISBN,
13: BookPages = book.Pages,
14: BookPubDate = book.PubDate,
15: BookPublisher = book.Publisher,
16: BookTitle = book.Title,
17: BookLent = book.Lent,
18: RegistrationDate = DateTime.Now,
19: DueDate = DateTime.Now.AddMonths(2)
20: });
21: }
22:
23: public void ReturnBook(Book book)
24: {
25: if (!book.Lent)
26: throw new DomainException();
27: if (!HasBorrowed(book))
28: throw new DomainException();
29: book.ReturnBy(this);
30: this.RaiseEvent<BookReturnedEvent>(new BookReturnedEvent
31: {
32: ReaderId = this.Id,
33: BookId = book.Id
34: });
35: }
36:
37: private bool HasBorrowed(Book book)
38: {
39: return Books.Any(p => p.Id.Equals(book.Id));
40: }
41:
42: [Handles(typeof(BookBorrowedEvent))]
43: private void OnBookBorrowed(BookBorrowedEvent evnt)
44: {
45: this.Books.Add(Book.Create(evnt.BookId,
46: evnt.BookTitle,
47: evnt.BookPublisher,
48: evnt.BookPubDate,
49: evnt.BookISBN,
50: evnt.BookPages,
51: evnt.BookLent));
52:
53: }
54:
55: [Handles(typeof(BookReturnedEvent))]
56: private void OnBookReturned(BookReturnedEvent evnt)
57: {
58: this.Books.RemoveAll(p => p.Id.Equals(evnt.BookId));
59: }
在上面定义的业务方法里,用到了RaiseEvent泛型方法。这个方法会通知Apworks框架已经产生了一个新的事件。领域对象有其自己的方法来处理这些新产生的事件,这些处理方法都被冠以Apworks.Events.HandlesAttribute特性。在这些事件处理方法中,领域对象的状态得到了改变。
目前看来,在这些领域对象中到处充斥着被标以Apworks.Events.HandlesAttribute特性的事件处理方法,当前的Apworks框架仅支持这样的基于反射技术的“事件-处理器”映射。在Apworks的后续版本中,会提供更多的映射策略以供开发人员选择。
总之,领域对象通过方法来表述它们需要处理的业务逻辑,这些方法又通过领域事件来更新领域对象的状态。这就使得CQRS架构能够通过领域事件来跟踪对象状态发生变化的情况。每当RaiseEvent泛型方法被调用时,SourcedAggregateRoot基类会将事件记录到本地的存储中,然后调用相应的标有Apworks.Events.HandlesAttribute特性的事件处理函数。当领域仓储保存聚合时,这些保存在本地的事件将被发布到事件总线,以便于订阅者能够对事件做后续处理。
在Apworks框架中,领域事件是由领域仓储推送到事件总线的【注意:这只是在Apworks的Alpha版本中会这么做,由于这么做会导致TPC(或者称2PC,二次提交)问题,所以在后续版本中会解决这个问题】。而在另一方面,系统会根据事件发布策略,将事件总线中的事件发布到与之相应的事件处理器上。这个过程可以通过异步实现以提高系统的响应度。当事件处理器获得了事件之后,它们会与基础结构层服务打交道以实现更多的功能(比如邮件推送、数据同步等等)。在Apworks系统启动的时候,应用程序就会根据配置文件对事件处理器进行注册。在后续章节中,我将简单介绍Apworks Alpha版本中的配置文件。
就TinyLibraryCQRS而言,我们尽创建一些用于同步“查询数据库”的事件处理器。查询数据库是一个位于基础结构层的存储系统,它用来保存与“查询”相关的数据,比如用于生成报表的数据,或者是用于显示在屏幕上的数据等。在后面的讨论中,你将了解到,TinyLibraryCQRS的查询数据库完全是为了迎合客户端的显示需要而设计的:每张数据表对应的是一个表示层的视图模型(View Model)。你会觉得这种设计将造成大量的数据冗余,没错,的确数据冗余很大,但这样做减少了JOIN操作甚至是ORM的引入,它有助于性能的提高【注意:在实际项目中,还是应该根据具体情况来确定查询数据库的设计方案】。
在创建事件处理器之前,我们先讨论一下“查询对象”的概念。“查询对象”可以看成是一种DTO(Data Transfer Object),它是查询数据库中数据的一种表现形式,会来回在网络中传输并持久化到存储系统。在Apworks框架的应用中,“查询对象”需要实现Apworks.Queries.IQueryObject接口,并使用System.SerializableAttribute特性来标识这些对象。不仅如此,如果你使用Apworks.Queries.Storage.SqlQueryObjectStorage来访问你的查询数据库(实际上,如果你是在使用关系型数据库系统),那么你就需要事先准备一个XML映射文件。这个XML映射文件告知系统,查询对象将被映射到哪张数据表,以及查询对象的属性将被映射到数据表中的哪些字段。这个XML文件的Schema结构非常简单,在后续章节中会对其进行讨论。
现在,让我们创建几个查询对象。
- 在 Solution Explorer 下,右键单击 TinyLibraryCQRS 解决方案,然后单击 Add | New Project… 菜单,这将打开 Add New Project 对话框
- 在 Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library,确保所选的.NET版本是.NET Framework 4,然后在 Name 文本框中,输入 TinyLibrary.QueryObjects,然后单击 OK 按钮
- 右键单击 TinyLibrary.QueryObjects 项目的 References 节点,单击 Add Reference… 菜单,这将打开 Add Reference 对话框
- 在 .NET 选项卡下,选择 Apworks 然后单击 OK 按钮
- 向 TinyLibrary.QueryObjects 项目添加如下代码
using System;
using System.Runtime.Serialization;
using Apworks.Queries;
namespace TinyLibrary.QueryObjects
{
[Serializable]
[DataContract]
public class BookObject : IQueryObject
{
[DataMember]
public long Id { get; set; }
[DataMember]
public string Title { get; set; }
[DataMember]
public string Publisher { get; set; }
[DataMember]
public DateTime PubDate { get; set; }
[DataMember]
public string ISBN { get; set; }
[DataMember]
public int Pages { get; set; }
[DataMember]
public bool Lent { get; set; }
[DataMember]
[DataMember]
public string LendTo { get; set; }
}
[Serializable]
[DataContract]
public class ReaderObject : IQueryObject
{
[DataMember]
public long Id { get; set; }
[DataMember]
public string LoginName { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public long AggregateRootId { get; set; }
}
[Serializable]
[DataContract]
public class RegistrationObject : IQueryObject
{
[DataMember]
public long Id { get; set; }
[DataMember]
public long BookAggregateRootId { get; set; }
[DataMember]
public long ReaderAggregateRootId { get; set; }
[DataMember]
public string ReaderName { get; set; }
[DataMember]
public string ReaderLoginName { get; set; }
[DataMember]
public string BookTitle { get; set; }
[DataMember]
public string BookPublisher { get; set; }
[DataMember]
public DateTime BookPubDate { get; set; }
[DataMember]
public string BookISBN { get; set; }
[DataMember]
public int BookPages { get; set; }
[DataMember]
public DateTime RegistrationDate { get; set; }
[DataMember]
public DateTime DueDate { get; set; }
[DataMember]
public DateTime ReturnedDate { get; set; }
[DataMember]
public bool Returned { get; set; }
}
}
现在可以开始创建事件处理器了。事件处理器会使用捕获的事件来创建查询对象,然后将其保存到查询数据库中。
- 在 Solution Explorer 中,右键单击 TinyLibraryCQRS 解决方案,然后单击 Add | New Project… 菜单,这将打开 Add New Project 对话框
- 在 Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library,确保所选的 .NET 版本是.NET Framework 4,在 Name 文本框中,输入 TinyLibrary.EventHandlers,然后单击 OK 按钮
- 右键单击 TinyLibrary.EventHandlers项目的 References 节点,然后单击 Add Reference… 菜单,这将打开 Add Reference 对话框
- 在.NET选项卡下,选择 Apworks 然后单击 OK 按钮
- 右键单击 TinyLibrary.EventHandlers项目的 References 节点,然后单击 Add Reference… 菜单,这将打开 Add Reference 对话框
- 在 Projects 选项卡下,选择 TinyLibrary.Events 和 TinyLibrary.QueryObjects,然后单击OK按钮
- 向 TinyLibrary.EventHandlers 项目加入如下代码:
using System;
using System.Data.SqlTypes;
using Apworks.Queries.Storage;
using Apworks.Storage;
using TinyLibrary.Events;
using TinyLibrary.QueryObjects;
namespace TinyLibrary.EventHandlers
{
public class BookBorrowedEventHandler : Apworks.Events.EventHandler<BookBorrowedEvent>
{
public override bool Handle(BookBorrowedEvent target)
{
using (IQueryObjectStorage storage = this.GetQueryObjectStorage())
{
RegistrationObject registrationObject = new RegistrationObject
{
BookAggregateRootId = target.BookId,
BookISBN = target.BookISBN,
BookPages = target.BookPages,
BookPubDate = target.BookPubDate,
BookPublisher = target.BookPublisher,
BookTitle = target.BookTitle,
ReaderAggregateRootId = target.ReaderId,
ReaderLoginName = target.ReaderLoginName,
ReaderName = target.ReaderName,
DueDate = target.DueDate,
RegistrationDate = target.RegistrationDate,
Returned = false,
ReturnedDate = (DateTime)SqlDateTime.MinValue
};
storage.Insert<RegistrationObject>(new Apworks.Storage.PropertyBag(registrationObject));
PropertyBag pbUpdateFields = new PropertyBag();
pbUpdateFields.Add("Lent", true);
pbUpdateFields.Add("LendTo", target.ReaderName);
PropertyBag pbCriteria = new PropertyBag();
pbCriteria.Add("AggregateRootId", target.BookId);
storage.Update<BookObject>(pbUpdateFields, pbCriteria);
}
return true;
}
}
public class BookCreatedEventHandler : Apworks.Events.EventHandler<BookCreatedEvent>
{
public override bool Handle(BookCreatedEvent target)
{
using (IQueryObjectStorage storage = this.GetQueryObjectStorage())
{
BookObject bookData = new BookObject
{
AggregateRootId = target.AggregateRootId,
ISBN = target.ISBN,
Lent = target.Lent,
Pages = target.Pages,
PubDate = target.PubDate,
Publisher = target.Publisher,
Title = target.Title,
LendTo = string.Empty
};
storage.Insert<BookObject>(new Apworks.Storage.PropertyBag(bookData));
}
return true;
}
}
public class BookReturnedEventHandler : Apworks.Events.EventHandler<BookReturnedEvent>
{
public override bool Handle(BookReturnedEvent target)
{
using (IQueryObjectStorage queryStorage = this.GetQueryObjectStorage())
{
PropertyBag pbCriteria = new PropertyBag();
pbCriteria.Add("BookAggregateRootId", target.BookId);
pbCriteria.Add("ReaderAggregateRootId", target.ReaderId);
PropertyBag pbUpdateFields = new PropertyBag();
pbUpdateFields.Add("ReturnedDate", DateTime.Now);
pbUpdateFields.Add("Returned", true);
queryStorage.Update<RegistrationObject>(pbUpdateFields, pbCriteria);
PropertyBag pbUpdateFieldsBooks = new PropertyBag();
pbUpdateFieldsBooks.Add("Lent", false);
pbUpdateFieldsBooks.Add("LendTo", string.Empty);
PropertyBag pbCriteriaBooks = new PropertyBag();
pbCriteriaBooks.Add("AggregateRootId", target.BookId);
queryStorage.Update<BookObject>(pbUpdateFieldsBooks, pbCriteriaBooks);
}
return true;
}
}
public class ReaderCreatedEventHandler : Apworks.Events.EventHandler<ReaderCreatedEvent>
{
public override bool Handle(ReaderCreatedEvent target)
{
using (IQueryObjectStorage storage = this.GetQueryObjectStorage())
{
ReaderObject readerObject = new ReaderObject
{ AggregateRootId = target.AggregateRootId, LoginName = target.LoginName, Name = target.Name };
storage.Insert<ReaderObject>(new PropertyBag(readerObject));
}
return true;
}
}
}
事件处理器需要继承Apworks.Events.EventHandler类,并实现Handle方法。在Handle方法中,事件处理器将首先通过GetQueryObjectStorage方法获得查询数据库存储对象,然后通过存储对象来保存查询对象。
至此,我们已经创建了聚合、快照、查询对象、领域事件以及事件处理器。在下一步操作中,我们将会创建一些“业务外观对象”来协调业务处理过程,以便更上层的应用组件能够方便地使用我们的领域模型。在CQRS架构中,这种“业务外观对象”就是命令与命令处理器。