[.NET領域驅動設計實戰系列]專題十:DDD擴展內容:全面剖析CQRS模式實現

原文: [.NET領域驅動設計實戰系列]專題十:DDD擴展內容:全面剖析CQRS模式實現

1、引言

   前面介紹的全部專題都是基於經典的領域驅動實現的,然而,領域驅動除了經典的實現外,還能夠基於CQRS模式來進行實現。本專題將全面剖析如何基於CQRS模式(Command Query Responsibility Segregation,命令查詢職責分離)來實現領域驅動設計。html

2、CQRS是什麼?

   在介紹具體的實現以前,對於以前不瞭解CQRS的朋友來講,首先第一個問題應該是:什麼是CQRS啊?你卻是詳細介紹完CQRS後再介紹具體實現啊?既然你們會有這樣的問題,因此本專題首先全面介紹下什麼是CQRS。git

  2.1 CQRS發展歷程

  在介紹CQRS以前,我以爲有必要先了解一下CQS(即Command Query Separation,命令查詢分離)模式。咱們能夠理解CQRS是在DDD的實踐中基於CQS理論而出現的一種體系結構模式。CQS模式最先由軟件大師Bertrand Meyer(Eiffel語言之父,面向對象開-閉原則OCP提出者)提出,他認爲,對象的行爲僅有兩種:命令和查詢,不存在第三種狀況。根據CQS的思想,任何方法均可以拆分爲命令和查詢兩部分。例以下面的方法:github

        private int _number = 0; public int Add(int factor) { _number += factor; return _number; }

  在上面的方法中,執行了一個命令,即對變量_number加上一個因子factor,同時又執行了一個查詢,即查詢返回_number的值。根據CQS的思想,該方法能夠拆成Command和Query兩個方法:面試

private int _number = 0;
private void AddCommand(int factor)
{
    _number += factor;
}

private int QueryValue()
{
    return _number;
}

  命令和查詢分離使得咱們能夠更好地把握對象的細節,更好地理解哪些操做會改變系統的狀態。從而使的系統具備更好的擴展性,並得到更好的性能。數據庫

  CQRS根據CQS思想,並結合領域驅動設計思想,由Grey Young在CQRS, Task Based UIs, Event Sourcing agh! 這篇文章中提出。CQRS將以前只須要定義一個對象拆分紅兩個對象,分離的原則按照對象中方法是執行命令仍是執行查詢來進行拆分的。app

  2.2 CQRS結構

  由前面的介紹可知,採用CQRS模式實現的系統結構能夠分爲兩個部分:命令部分和查詢部分。其系統結構以下圖所示:框架

  從上面系統結構圖能夠發現,採用CQRS實現的領域驅動設計與經典DDD有很大的不一樣。採用CQRS實現的DDD結構大致分爲兩部分,查詢部分和命令部分,而且維護着兩個數據庫實例,一個專門用來進行查詢,另外一個用來響應命令操做。而後經過EventHandler操做將命令改變的狀態同步到用來查詢的數據庫實例中。從這個描述中,咱們可能會聯想到數據庫級別主從讀寫分離。然而數據讀寫分離是在數據庫層面來實現讀寫分離的機制,而CQRS是在業務邏輯層面來實現讀寫分離機制。二者是站在兩個不一樣的層面對讀寫分離進行實現的。dom

3、爲何須要引入CQRS模式

   前面咱們已經詳細介紹了CQRS模式,相信通過前面的介紹,你們對CQRS模式必定有一些瞭解了,但爲何要引入CQRS模式呢?
性能

  在傳統的實現中,對DB執行增、刪、改、查全部操做都會放在對應的倉儲中,而且這些操做都公用一份領域實體對象。對於一些簡單的系統,使用傳統的設計方式並無什麼不妥,但在一些大型複雜的系統中,傳統的實現方式也會存在一些問題:學習

  • 使用同一個領域實體來進行數據讀寫可能會遇到資源競爭的狀況。因此常常要處理鎖的問題,在寫入數據的時候,須要加鎖,讀取數據的時候須要判斷是否容許髒讀。這樣使得系統的邏輯性和複雜性增長,並會影響系統的吞吐量。
  • 在大數據量同時進行讀寫的狀況下,可能出現性能的瓶頸。
  • 使用同一個領域實體來進行數據庫讀寫可能會太粗糙。在大可能是狀況下,好比編輯操做,可能只須要更新個別字段,這時卻須要將整個對象都穿進去。還有在查詢的時候,表現層可能只須要個別字段,但須要查詢和返回整個領域實體,再把領域實體對象轉換從對應的DTO對象。
  • 讀寫操做都耦合在一塊兒,不利於對問題的跟蹤和分析,若是讀寫操做分離的話,若是是因爲狀態改變的問題就只須要去分析寫操做相關的邏輯就能夠了,若是是關於數據的不正確,則只須要關心查詢操做的相關邏輯便可。

  針對上面的這些問題,採用CQRS模式的系統均可以解決。因爲CQRS模式中將查詢和命令進行分析,因此使得二者分工明確,各自負責不一樣的部分,而且在業務上將命令和查詢分離可以提升系統的性能和可擴展性。既然CQRS這麼好,那是否是全部系統都應該基於CQRS模式去實現呢?顯然不是的,CQRS也有其使用場景:

  1. 系統的業務邏輯比較複雜的狀況下。由於原本業務邏輯就比較複雜了,若是再把命令操做和查詢操做綁定同一個業務實體的話,這樣會致使後期的需求變動難於進行擴展下去。
  2. 須要對系統中查詢性能和寫入性能分開進行優化的狀況下,尤爲讀/寫比例很是高的狀況下。例如,在不少系統中讀操做的請求數遠大於寫操做,此時,就能夠考慮將寫操做抽離出來進行單獨擴展。
  3. 系統在未來隨着時間不斷變化的狀況下。

  然而,CQRS也有其不適用的場景:

  • 業務邏輯比較簡單的狀況下,此時採用CQRS反而會把系統搞的複雜。
  • 系統用戶訪問量都比較小的狀況下,而且需求之後不怎麼會變動的狀況下。針對這樣的系統,徹底能夠用傳統的實現方式快速將系統實現出來,不必引入CQRS來增長系統的複雜度。

4、事件溯源

  在CQRS中,查詢方面,直接經過方法查詢數據庫,而後經過DTO將數據返回,這個方面的操做相對比較簡單。而命令方面,是經過發送具體Command,接着由CommandBus來分發到具體的CommandHandle來進行處理,CommandHandle在進行處理時,並無直接將對象的狀態保存到外部持久化結構中,而僅僅是從領域對象中得到產生的一系列領域事件,並將這些事件保存到Event Store中,同時將事件發佈到事件總線Event Bus進行下一步處理;接着Event Bus一樣進行協調,將具體的事件交給具體的Event Handle進行處理,最後Event Handler再把對象的狀態保存到對應Query數據庫中。

  上面過程正是CQRS系統中的調用順序。從中能夠發現,採用CQRS實現的系統存在兩個數據庫實例,一個是Event Store,該數據庫實例用來保存領域對象中發生的一系列的領域事件,簡單來講就是保存領域事件的數據庫。另外一個是Query Database,該數據庫就是存儲具體的領域對象數據的,查詢操做能夠直接對該數據庫進行查詢。因爲,咱們在Event Store中記錄領域對象發生的全部事件,這樣咱們就能夠經過查詢該數據庫實例來得到領域對象以前的全部狀態了。所謂Event Sourcing,就是指的的是:經過事件追溯對象的起源,它容許經過記錄下來的事件,將領域模型恢復到以前的任意一個時間點。

  經過Event來記錄領域對象所發生的全部狀態,這樣利用系統的跟蹤並可以方便地回滾到某一歷史狀態。通過上面的描述,感受事件溯源通常用於系統的維護。例如,咱們能夠設計一個同步服務,該服務程序從Event Store數據庫查詢出領域對象的歷史數據,從而打印生成一個歷史報表,如歷史價格報表等。但正是的CQRS系統中如何使用Event Sourcing的呢?

  在前面介紹CQRS系統的調用順序中,咱們講到,由Event Handler將對象的狀態保存到對應的Query數據庫中,這裏有一個問題,對象的狀態怎麼得到呢?對象狀態的得到正是由Event sourcing機制來得到,由於用戶發送的僅僅是Command,Command中並不包含對象的狀態數據,因此此時須要經過Event Sourcing機制來查詢Event Store來還原對象的狀態,還原根據就是對應的Id,該Id是經過命令傳入的。Event Sourcing的調用須要放在CommandHandle中,由於CommandHandle須要先得到領域對象,這樣才能把領域對象與命令對象來進行對比,從而得到領域對象中產生的一系列領域事件。

5、快照

   然而,當隨着時間的推移,領域事件變得愈來愈多時,經過Event Sourcing機制來還原對象狀態的過程會很是耗時,由於每一次都須要從最先發生的事件開始。那有沒有好的一個方式來解決這個問題呢?答案是確定的,即在Event Sourcing中引入快照(Snapshots)實現。實現原理就是——沒產生N個領域事件,則對對象作一次快照。這樣,領域對象溯源的時候,能夠先從快照中得到最近一次的快照,而後再逐個應用快照以後全部產生的領域事件,而不須要每次溯源都從最開始的事件開始對對象重建,這樣就大大加快了對象重建的過程。

6、CQRS模式實現和剖析

  前面介紹了那麼多CQRS的內容,下面就具體經過一個例子來演示下CQRS系統的實現。

  命令部分的實現

  

    // 應用程序初始化操做,將依賴的對象經過依賴注入框架StructureMap進行注入
    public sealed class ServiceLocator
    {
        private static readonly ICommandBus _commandBus;
        private static readonly IStorage _queryStorage;
        private static readonly bool IsInitialized;
        private static readonly object LockThis = new object();
        
        static ServiceLocator()
        {
            if (!IsInitialized)
            {
                lock (LockThis)
                {
                    // 依賴注入
                    ContainerBootstrapper.BootstrapStructureMap();

                    _commandBus = ContainerBootstrapper.Container.GetInstance<ICommandBus>();
                    _queryStorage = ContainerBootstrapper.Container.GetInstance<IStorage>();
                    IsInitialized = true;
                }
            }
        }

        public static ICommandBus CommandBus
        {
            get { return _commandBus; }
        }

        public static IStorage QueryStorage
        {
            get { return _queryStorage; }
        }
    }

    class ContainerBootstrapper
    {
        private static Container _container;
        public static void BootstrapStructureMap()
        {
            _container = new Container(x =>
            {
                x.For(typeof (IDomainRepository<>)).Singleton().Use(typeof (DomainRepository<>));
                x.For<IEventStorage>().Singleton().Use<InMemoryEventStorage>();
                x.For<IEventBus>().Use<EventBus>();
                x.For<ICommandBus>().Use<CommandBus>();
                x.For<IStorage>().Use<InMemoryStorage>();
                x.For<IEventHandlerFactory>().Use<StructureMapEventHandlerFactory>();
                x.For<ICommandHandlerFactory>().Use<StructureMapCommandHandlerFactory>();
            });
        }

        public static Container Container 
        {
            get { return _container;}
        }
    }

public class HomeController : Controller
    {
         [HttpPost]
        public ActionResult Add(DiaryItemDto item)
        {
            // 發佈CreateItemCommand到CommandBus中
            ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));

            return RedirectToAction("Index");
        }    
    }

 // CommandBus 的實現
    public class CommandBus : ICommandBus
    {
        private readonly ICommandHandlerFactory _commandHandlerFactory;

        public CommandBus(ICommandHandlerFactory commandHandlerFactory)
        {
            _commandHandlerFactory = commandHandlerFactory;
        }

        public void Send<T>(T command) where T : Command
        {
            // 得到對應的CommandHandle來對命令進行處理
            var handlers = _commandHandlerFactory.GetHandlers<T>();

            foreach (var handler in handlers)
            {
                // 處理命令
                handler.Execute(command);
            }
        }       
    }

// 對CreateItemCommand處理類
    public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>
    {
        private readonly IDomainRepository<DiaryItem> _domainRepository;

        public CreateItemCommandHandler(IDomainRepository<DiaryItem> domainRepository)
        {
            _domainRepository = domainRepository;
        }

        // 具體處理邏輯
        public void Execute(CreateItemCommand command)
        {
            if (command == null)
            {
                throw new ArgumentNullException("command");
            }
            if (_domainRepository == null)
            {
                throw new InvalidOperationException("domainRepository is not initialized.");
            }

            var aggregate = new DiaryItem(command.ID, command.Title, command.Description, command.From, command.To)
            {
                Version = -1
            };

            // 將對應的領域實體進行保存
            _domainRepository.Save(aggregate, aggregate.Version);
        }
    }

 // IDomainRepository的實現類
    public class DomainRepository<T> : IDomainRepository<T> where T : AggregateRoot, new()
    {
             // 並無直接對領域實體進行保存,而是先保存領域事件進EventStore,而後在Publish事件到EventBus進行處理
        // 而後EventBus把事件分配給對應的事件處理器進行處理,由事件處理器來把領域對象保存到QueryDatabase中
        public void Save(AggregateRoot aggregate, int expectedVersion)
        {
            if (aggregate.GetUncommittedChanges().Any())
            {
                _storage.Save(aggregate);
            }
        }
    }

 // Event Store的實現,這裏保存在內存中,一般是保存到具體的數據庫中,如SQL Server、Mongodb等
    public class InMemoryEventStorage : IEventStorage
    {
         // 領域事件的保存
        public void Save(AggregateRoot aggregate)
        {
            // 得到對應領域實體未提交的事件
            var uncommittedChanges = aggregate.GetUncommittedChanges();
            var version = aggregate.Version;

            
            foreach (var @event in uncommittedChanges)
            {
                version++;
                // 沒3個事件建立一次快照
                if (version > 2)
                {
                    if (version % 3 == 0)
                    {
                        var originator = (ISnapshotOrignator)aggregate;
                        var snapshot = originator.CreateSnapshot();
                        snapshot.Version = version;
                        SaveSnapshot(snapshot);
                    }
                }

                @event.Version = version;
                // 保存事件到EventStore中
                _events.Add(@event);
            }

            // 保存事件完成以後,再將該事件發佈到EventBus 作進一步處理
            foreach (var @event in uncommittedChanges)
            {
                var desEvent = TypeConverter.ChangeTo(@event, @event.GetType());
                _eventBus.Publish(desEvent);
            }
        }
    }

  // EventBus的實現
    public class EventBus : IEventBus
    {
        private readonly IEventHandlerFactory _eventHandlerFactory;

        public EventBus(IEventHandlerFactory eventHandlerFactory)
        {
            _eventHandlerFactory = eventHandlerFactory;
        }

        public void Publish<T>(T @event) where T : DomainEvent
        {
            // 得到對應的EventHandle來處理事件
            var handlers = _eventHandlerFactory.GetHandlers<T>();
            foreach (var eventHandler in handlers)
            {
                // 對事件進行處理
                eventHandler.Handle(@event);
            }
        }
    }

// DiaryItemCreatedEvent的事件處理類
    public class DiaryIteamCreatedEventHandler : IEventHandler<DiaryItemCreatedEvent>
    {
        private readonly IStorage _storage;

        public DiaryIteamCreatedEventHandler(IStorage storage)
        {
            _storage = storage;
        }

        public void Handle(DiaryItemCreatedEvent @event)
        {
            var item = new DiaryItemDto()
            {
                Id = @event.SourceId,
                Description = @event.Description,
                From = @event.From,
                Title = @event.Title,
                To = @event.To,
                Version = @event.Version
            };

            // 將領域對象持久化到QueryDatabase中
            _storage.Add(item);
        }
    }
    

  上面代碼主要演示了Command部分的實現,從代碼能夠看出,首先咱們須要經過ServiceLocator類來對依賴注入對象進行注入,而後UI層經過CommandBus把對應的命令發佈到CommandBus中進行處理,命令總線再查找對應的CommandHandler來對命令進行處理,接着CommandHandler調用倉儲類來保存領域對象對應的事件,保存事件成功後再將事件發佈到事件總線中進行處理,而後由對應的事件處理程序將領域對象保存到QueryDatabase中。這樣就完成了命令部分的操做,從中能夠發現,命令部分的實現和CQRS系統中的系統結構圖的處理過程是同樣的。然而建立日誌命令並無涉及事件溯源操做,由於建立命令並須要重建領域對象,此時的領域對象是經過建立日誌命令來得到的,但在修改和刪除命令中涉及了事件溯源,由於此時須要根據命令對象的ID來重建領域對象。具體的實現能夠參考源碼。

  下面讓咱們再看看查詢部分的實現。

  查詢部分的實現代碼:

 public class HomeController : Controller
    {
        // 查詢部分
        public ActionResult Index()
        {
            // 直接得到QueryDatabase對象來查詢全部日誌
            var model = ServiceLocator.QueryStorage.GetItems();
            return View(model);
        }
    }

 public class InMemoryStorage : IStorage
    {
        private static readonly List<DiaryItemDto> Items = new List<DiaryItemDto>();

        public DiaryItemDto GetById(Guid id)
        {
            return Items.FirstOrDefault(a => a.Id == id);
        }

        public void Add(DiaryItemDto item)
        {
            Items.Add(item);
        }

        public void Delete(Guid id)
        {
            Items.RemoveAll(i => i.Id == id);
        }

        public List<DiaryItemDto> GetItems()
        {
            return Items;
        }
    }

  從上面代碼能夠看出,查詢部分的代碼實現相對比較簡單,UI層直接經過QueryDatabase來查詢領域對象,而後由UI層進行渲染出來顯示。

  到此,一個簡單的CQRS系統就完成了,然而在項目中,UI層並不會直接CommandBus和QueryDatabase進行引用,而是經過對應的CommandService和QueryService來進行協調,具體的系統結構以下圖所示(只是在CommandBus和Query Database前加入了一個SOA的服務層來進行協調,這樣有利於系統擴展,能夠經過SOA服務來進行請求路由,將不一樣請求路由不一樣的系統中,這樣會能夠實現多個系統進行一個整合):

  關於該CQRS系統的演示效果,你們能夠自行去Github或MSDN中進行下載,具體的下載地址將會本專題最後給出。

7、總結

   到這裏,本專題關於CQRS的介紹就結束了,而且本專題也是領域驅動設計系列的最後一篇了。本系列專題的內容主要是參考daxnet的ByteartRetail案例,因爲daxnet在寫這個案例的時候並無一步一步介紹其建立過程,對於一些領域驅動的初學者來講,直接去學習這個案例未免會有點困難,致使學習興趣下降,從而放棄領域驅動的學習。爲了解決這些問題,因此,本人對ByteartRetail案例進行剖析,並參考該案例一步步實現本身的領域驅動案例OnlineStore。但願本系列能夠幫助你們打開領域驅動的大門。

  因爲如今NO-SQL在互聯網行業的應用已經很是流行,以致於面試的時候常常會被問到你用過的非關係數據庫有哪些?因此本人也不想Out,因此在最近2個月的時候學習了一些No-SQL的內容,因此,接下來,我將會開啓一個NO-SQL系列,記錄本身這段時間來學習NO-SQL的一些心得和體會。

 

  本專題全部源碼下載:

  Github地址:https://github.com/lizhi5753186/CQRSDemo

   MSDN地址:https://code.msdn.microsoft.com/CQRS-1f05ebe5

     本文參考連接:

     http://www.codeproject.com/Articles/555855/Introduction-to-CQRS 

     http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html

相關文章
相關標籤/搜索