使用Apworks开发基于CQRS架构的应用程序(四):领域事件

根据wikipedia中关于“事件”的描述,“事件”可以被看成是“状态的一次变化”。例如:当一个客户购买了一台汽车,汽车的状态就从“待售”转变为“已售”。汽车销售系统则把这种状态的改变看成是一次事件的产生、发布、检测以及被更多其它应用程序所使用的过程。

对于CQRS架构的应用程序而言,事件产生于领域模型,并由领域模型发布事件同时由领域模型首次捕获并处理,因此,我们称之为领域事件(Domain Events)。在Apworks开发框架中,与领域事件相关的代码都被定义在Apworks.Events命名空间下。

请使用下面的步骤为TinyLibraryCQRS解决方案添加领域事件。

  1. Solution ExplorerTinyLibraryCQRS 解决方案上,右键单击并选择 Add | New Project… 菜单,这将打开 Add New Project 对话框
  2. Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library,确保所选的.NET Framework版本是.NET Framework 4,然后在 Name 文本框中,输入 TinyLibrary.Events,并单击OK按钮
  3. Solution Explorer 下,右键单击TinyLibrary.Events项目的 References 节点,然后选择 Add Reference… 菜单,这将打开 Add Reference 对话框
  4. .NET选项卡下,选择Apworks,然后单击OK按钮
  5. 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方法。

  1. 回到 TinyLibrary.Domain 项目,右键单击 References 节点,选择 Add Reference… 菜单,这将打开 Add Reference 对话框
  2. Projects 选项卡下,选择 TinyLibrary.Events,然后单击OK按钮
  3. 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: }
  4. 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结构非常简单,在后续章节中会对其进行讨论。

现在,让我们创建几个查询对象。

  1. Solution Explorer 下,右键单击 TinyLibraryCQRS 解决方案,然后单击 Add | New Project… 菜单,这将打开 Add New Project 对话框
  2. Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library,确保所选的.NET版本是.NET Framework 4,然后在 Name 文本框中,输入 TinyLibrary.QueryObjects,然后单击 OK 按钮
  3. 右键单击 TinyLibrary.QueryObjects 项目的 References 节点,单击 Add Reference… 菜单,这将打开 Add Reference 对话框
  4. .NET 选项卡下,选择 Apworks 然后单击 OK 按钮
  5. 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; }
        }
    }

现在可以开始创建事件处理器了。事件处理器会使用捕获的事件来创建查询对象,然后将其保存到查询数据库中。

  1. Solution Explorer 中,右键单击 TinyLibraryCQRS 解决方案,然后单击 Add | New Project… 菜单,这将打开 Add New Project 对话框
  2. Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library,确保所选的 .NET 版本是.NET Framework 4,在 Name 文本框中,输入 TinyLibrary.EventHandlers,然后单击 OK 按钮
  3. 右键单击 TinyLibrary.EventHandlers项目的 References 节点,然后单击 Add Reference… 菜单,这将打开 Add Reference 对话框
  4. .NET选项卡下,选择 Apworks 然后单击 OK 按钮
  5. 右键单击 TinyLibrary.EventHandlers项目的 References 节点,然后单击 Add Reference… 菜单,这将打开 Add Reference 对话框
  6. Projects 选项卡下,选择 TinyLibrary.EventsTinyLibrary.QueryObjects,然后单击OK按钮
  7. 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架构中,这种“业务外观对象”就是命令与命令处理器。

posted @ 2011-01-26 16:10 dax.net 阅读(...) 评论(...) 编辑 收藏