CQRS体系结构模式实践案例:Tiny Library:对象的行为和状态
从结构上看,tlibcqrs项目并不复杂,但对其进行介绍,的确让我感到有点无从着手。还是先从领域模型中的对象的行为和状态谈起吧。
先来谈谈对象状态。据我理解,状态就是一种数据,它用来描述,在某个特定的时间上,这个对象所具有的特质,它将作为对象行为发生的依据和结果。我们平时做设计和编程的时候,尤其是在做数据访问层的时候,特别喜欢一些仅仅包含getter/setter属性的对象,以便调用方能够通过getter获得对象的状态,使用setter设置对象的状态。之前我也说明过,状态并非getter/setter属性,在OOP上,状态表现为“字段”(fields)。现在我们讨论的不是数据访问层的DAO,而是领域模型中的实体。当然,实体也是对象,自然也有状态,不仅仅是状态,实体是参与业务逻辑的重要对象,它还有处理业务逻辑的行为。
现在假设我们有个实体为Customer,它同时也是某个聚合的聚合根,在通常情况下,我们会用下面的形式去定义这个Customer实体(为了简化,省去了对象行为):
当然你不会觉得这样设计有什么太大的问题,事实上在我们平时的开发中,也的确是这么做的,而且非CQRS架构的DDD实践也支持这样的实体模型。于是,我们可以使用下面的代码来更新某个Customer的姓名:
1: [TestMethod]
2: public void ChangeCustomerNameTest()
3: {
4: Customer customer = new Customer
5: {
6: Birth = DateTime.Now.AddYears(-20),
7: Email = "daxnet@live.com",
8: FirstName = "dax",
9: LastName = "net",
10: Password = "123456",
11: Username = "daxnet"
12: };
13: using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())
14: {
15: IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();
16: customerRepository.Add(customer);
17: }
18: ISpecification<Customer> spec = Specification<Customer>.Eval(p => p.Username.Equals("daxnet"));
19: using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())
20: {
21: IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();
22: var customer2 = customerRepository.Get(spec);
23: Assert.AreEqual(customer.Username, customer2.Username);
24:
25: customer2.FirstName = "qingyang";
26: customer2.LastName = "chen";
27: customerRepository.Update(customer2);
28: }
29: using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())
30: {
31: IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();
32: var customer3 = customerRepository.Get(spec);
33: Assert.AreEqual("qingyang", customer3.FirstName);
34: Assert.AreEqual("chen", customer3.LastName);
35: }
36: }
在上面代码段的25行和26行,我们直接设置了Customer的姓名,然后在27行调用仓储进行实体更新。一切都非常顺利。今后,实体中的属性,就成为了我们实现业务逻辑和实体行为的依据。然而,CQRS体系结构模式的实践就与这种方式大相径庭。CQRS是一种事件驱动的架构(Event Driven Architecture,EDA),它的设计与实践需要遵循事件驱动的基本特征。
事件驱动架构下的对象状态
事件驱动架构是一种体系结构模式,它对事件的整个生命周期进行检测、跟踪和管理,并对事件的产生与发展做出反应。“事件”,可以定义为“造成状态变化的信号”。比如:当用户买了一辆轿车,那么,这辆车的状态就从“等待销售”转变为“已出售”。汽车销售系统就会将此作为一种事件将其发布到事件总线,以便应用程序的其它系统或组件能够订阅到这个事件并做进一步的处理(请参见维基百科:http://en.wikipedia.org/wiki/Event_driven_architecture)。
从现在开始,我们需要重新认识对象状态的变化。由于EDA的引入,对象状态只能通过事件的发生而产生变化,外界不能无缘无故地对其进行改变(就像上面的例子一样)。这样做的好处是:我们不仅能够知道对象的当前状态,而且还能知道,到底发生了哪些事情,才使对象变成现在这副“模样”。当某一事件发生时,对象捕获到这一事件并对其进行处理,而在处理的过程中再根据事件的类型和数据来改变自己的状态。由于通常情况下,这样的处理过程是对象的内部行为,因此,我们也就无需将更改状态的接口暴露给外部。在理解了这部分内容后,我们的Customer对象的设计就需要做出修改,变成下面这种形式(为了讨论方便,在此将Customer实体命名为SourcedCustomer,意为支持事件溯源):
1: public class SourcedCustomer : SourcedAggregateRoot
2: {
3: public virtual string Username { get; private set; }
4: public virtual string Password { get; private set; }
5: public virtual string FirstName { get; private set; }
6: public virtual string LastName { get; private set; }
7: public virtual string Email { get; private set; }
8: public virtual DateTime Birth { get; private set; }
9: }
将setter定义为private,以防止外界直接修改对象状态。对于某些外部也不关心的状态,我们甚至连getter都可以省去(也就是不需要再实现为property了),取而代之的是一个private的字段。比如:
1: public class SourcedCustomer : SourcedAggregateRoot
2: {
3: private DateTime dayOfBirth;
4: // ....
5: }
对象的行为导致状态变化
现在回到tlibcqrs项目,让我们看看TinyLibrary.Domain下Book实体的实现方式。它的状态是一系列的public getter和private setter的自动实现的属性。Book实体状态的改变,是通过其行为实现的。当某个行为被外界调用时,行为本身会产生一个事件,而对象本身又去处理这个事件,从而导致状态变化。
1: public class Book : SourcedAggregateRoot
2: {
3: public string Title { get; private set; }
4: public string Publisher { get; private set; }
5: public DateTime PubDate { get; private set; }
6: public string ISBN { get; private set; }
7: public int Pages { get; private set; }
8: public bool Lent { get; private set; }
9:
10: public Book() : base() { }
11: public Book(long id) : base(id) { }
12:
13: public static Book Create(string title, string publisher, DateTime pubDate, string isbn, int pages, bool lent)
14: {
15: Book book = new Book();
16: book.RaiseEvent<BookCreatedEvent>(new BookCreatedEvent
17: {
18: Title = title,
19: Publisher = publisher,
20: PubDate = pubDate,
21: ISBN = isbn,
22: Pages = pages,
23: Lent = lent
24: });
25: return book;
26: }
27:
28: public static Book Create(long id, string title, string publisher, DateTime pubDate, string isbn, int pages, bool lent)
29: {
30: Book book = new Book(id);
31: book.RaiseEvent<BookCreatedEvent>(new BookCreatedEvent
32: {
33: Title = title,
34: Publisher = publisher,
35: PubDate = pubDate,
36: ISBN = isbn,
37: Pages = pages,
38: Lent = lent
39: });
40: return book;
41: }
42:
43: public void LendTo(Reader reader)
44: {
45: this.RaiseEvent<BookLentEvent>(new BookLentEvent { ReaderId = reader.Id, LentDate = DateTime.Now });
46: }
47:
48: public void ReturnBy(Reader reader)
49: {
50: this.RaiseEvent<BookGetReturnedEvent>(new BookGetReturnedEvent { ReaderId = reader.Id, ReturnedDate = DateTime.Now });
51: }
52:
53: #region Domain Event Handlers
54: [Handles(typeof(BookCreatedEvent))]
55: private void OnBookCreated(BookCreatedEvent evnt)
56: {
57: this.Title = evnt.Title;
58: this.Publisher = evnt.Publisher;
59: this.PubDate = evnt.PubDate;
60: this.ISBN = evnt.ISBN;
61: this.Pages = evnt.Pages;
62: this.Lent = evnt.Lent;
63: }
64: [Handles(typeof(BookLentEvent))]
65: private void OnBookLent(BookLentEvent evnt)
66: {
67: this.Lent = true;
68: }
69: [Handles(typeof(BookGetReturnedEvent))]
70: private void OnBookReturnedBack(BookGetReturnedEvent evnt)
71: {
72: this.Lent = false;
73: }
74: #endregion
75: }
上面的代码就是TinyLibrary.Domain.Book实体,我们可以看到,在Book被创建的时候,会产生BookCreatedEvent事件,当Book被借出时,会产生BookLentEvent事件,当Book被归还时,又会产生BookGetReturnedEvent事件。而这些事件会被OnBookCreated、OnBookLent和OnBookReturnedBack私有方法捕获,从而引起对象状态的变化。
不仅如此,SourcedAggregateRoot基类会将这些事件记录下来。领域仓储(Domain Repository)在保存聚合的时候,就只需要保存这一系列事件就可以了。这也是为什么在tlibcqrs项目中有一个TinyLibraryEventDB的数据库,而这个数据库却只有一张DomainEvents数据表的原因。由于实体本身能够通过捕获并处理事件来恢复状态,因此,通过事件回放即可重塑实体。当然,在领域仓储保存聚合的同时,这些事件也会被推送到事件总线,以便系统的其它部分能够对这些事件进行处理。
有关领域仓储(Domain Repository)和事件存储(Event Store)相关的内容和疑问,将在下一讲中进行讲解,敬请期待!