ENode 1.0 - 事件驅動架構(EDA)思想的在框架中如何體現

開源地址:https://github.com/tangxuehua/enodehtml

上一篇文章,我給你們分享了個人一個基於DDD以及EDA架構的框架enode,可是隻是介紹了一個大概。接下來我準備用不少一篇篇詳細但不冗長的文章介紹每一個點。儘可能爭取一次不介紹太多內容,但但願每次介紹完後都能讓你們知道這個小點的設計思想,以及爲了解決的問題。node

好了,這篇文章,我主要想介紹的是EDA思想在enode框架中如何體現?git

經典DDD的基於領域服務的實現方式

通常的應用程序,若是一個用戶動做會涉及多個聚合根的修改,咱們一般會在應用層服務中建立一個unit of work,而後,咱們可能會設計一個領域服務類,在該領域服務類裏,修改多個聚合根,而後應用層服務將整個unit of work中的修改一次性以事務的方式提交到數據庫。這種方式就是以事務的方式來實現涉及多個聚合根修改的強一致性。以銀行轉帳這個經典的場景做爲分析案例:github

    public interface IBankAccountService
    {
        void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount);
    }
    public class BankAccountService : IBankAccountService
    {
        private IContextManager _contextManager;
        private TransferMoneyService _transferMoneyService;

        public BankAccountService(IContextManager contextManager, TransferMoneyService transferMoneyService)
        {
            _contextManager = contextManager;
            _transferMoneyService = transferMoneyService;
        }

        public void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount)
        {
            using (var context = _contextManager.GetContext())
            {
                var sourceAccount = context.Load<BankAccount>(sourceBankAccountId);
                var targetAccount = context.Load<BankAccount>(targetBankAccountId);
                _transferMoneyService.TransferMoney(sourceAccount, targetAccount, amount);
                context.SaveChanges();
            }
        }
    }

一次銀行轉帳,最核心的動做就是源帳號轉出錢,目標帳號轉入錢;固然實際的銀行轉帳確定不是這麼簡單,也確定不是這麼實現。我拿這個做爲例子只是爲了經過這個你們都熟知的簡單例子來分析若是一個用戶場景涉及不止一個聚合根的修改的時候,若是基於經典的DDD的方式,咱們是如何實現的。如上面的代碼所示,咱們可能會設計一個應用層服務,如上面的IBankAccountService,該應用層服務裏有一個TransferMoney的方法,表示用於實現銀行轉帳的功能;而後該應用層服務會進一步調用一個領域層的轉帳領域服務,就是上面代碼中的TransferMoneyService,按照Eric Evans所說,領域服務應該是一個以動詞命名的服務,一個領域服務能夠明確對應到領域中的一個有業務含義的領域動做,此例就是「轉帳」,因此我設計了一個TransferMoneyService的以動詞來命名的領域服務,該服務的TransferMoney方法實現了銀行轉帳的核心業務邏輯。數據庫

上面這個例子中,按照經典DDD,咱們應該在應用層實現流程控制邏輯以及事務等東西;因此你們能夠看到,以上代碼中,咱們是先獲取一個unit of work,即上面代碼中的context,最後調用context.SaveChanges方法,該方法的職責就是將當前上下文的全部修改以事務的方式提交到數據庫。好了,上面這個例子咱們分析了經典DDD關於如何實現一個會涉及多個聚合根新建或修改的用戶場景;緩存

enode的事件驅動的實現方式

我一直說enode是一個基於事件驅動架構(EDA,Event-Driven Architecture)的框架。且深藍醫生在前面的回覆中也對什麼是事件驅動的架構有疑惑。因此我想說一下我對事件驅動架構的理解。架構

EDA,顧名思義,我以爲就是事件驅動的,那事件到底驅動了什麼呢?我以爲就是事件驅動狀態的修改。如何理解呢?就是說,假如你要修改一個對象的狀態,那就不是直接調用該對象的某個方法來修改它或者直接經過修改某個對象的屬性來達到修改該對象狀態的目的;取而代之的是,咱們須要先觸發一個事件,而後該對象會響應該事件,而後在響應函數中修改對象本身的狀態。固然,更廣義和權威的事件驅動架構的定義和解釋,我以爲很容易找啊,好比直接去百度上搜一下或直接到wikipedia上搜一下,也很容易就能找到標準的解釋。好比這裏就是我找到的解釋。其實,更大範圍的解釋,就是一種publish-subscriber模式,就是有一個事件生產者產生事件,而後有一個相似event publisher的東西會把這個事件廣播出去,而後全部的事件消費者就能消費該事件了。經過這樣的pub-sub,咱們的應用程序的各個組件之間能夠作到很完全的解耦,而且能夠作到更靈活的擴展性。這兩點的好處應該是很容易體會到的。好比更完全的解耦是,好比原本一個對象要和另外一個對象交互,那它可能要引用該對象,而後調用該對象的某個方法,從而實現對象之間的交互。這種實現方式會讓兩個對象綁定在一塊兒,好比a對象調用b對象的方法,那意味着a須要依賴b對象;而經過事件驅動的方式,a對象只要publish一個事件,而後b對象響應該事件便可,這樣a對象就不知道b對象的存在了,也就是a對象不在依賴b對象;擴展性,就是原本一個事件,可能只有1個事件響應者,可是後面可能因爲功能擴展等緣由,咱們須要增長一個事件響應者,這樣就能方便的作到在不改變原來任何代碼的基礎之上,增長新功能了;其餘的好處就很少分析了,有興趣的能夠再去看看資料吧。併發

上面這一段,我簡單介紹了我所理解的EDA,以及它的基本的好處。下面咱們看看,在enode中,咱們是如何利用EDA這種原理的。爲了簡化,我先用一個簡單的例子說明一下,就用我源代碼中的NoteSample吧,反正也能同樣說明事件驅動的影子在哪裏。看如下的代碼:框架

    [Serializable]
    public class Note : AggregateRoot<Guid>,
        IEventHandler<NoteCreated>,     //訂閱事件
        IEventHandler<NoteTitleChanged>
    {
        public string Title { get; private set; }
        public DateTime CreatedTime { get; private set; }
        public DateTime UpdatedTime { get; private set; }

        public Note() : base() { }
        public Note(Guid id, string title) : base(id)
        {
            var currentTime = DateTime.Now;
            //觸發事件
            RaiseEvent(new NoteCreated(Id, title, currentTime, currentTime));
        }

        public void ChangeTitle(string title)
        {
            //觸發事件
            RaiseEvent(new NoteTitleChanged(Id, title, DateTime.Now));
        }

        //事件響應函數
        void IEventHandler<NoteCreated>.Handle(NoteCreated evnt)
        {
            //在響應函數中修改本身的狀態,這裏能夠體現出EDA的影子,就是事件驅動狀態的修改
            Title = evnt.Title;
            CreatedTime = evnt.CreatedTime;
            UpdatedTime = evnt.UpdatedTime;
        }
        //事件響應函數
        void IEventHandler<NoteTitleChanged>.Handle(NoteTitleChanged evnt)
        {
            //同上解釋
            Title = evnt.Title;
            UpdatedTime = evnt.UpdatedTime;
        }
    }

 

上面的例子中,Note是一個聚合根,它會響應兩個事件:NoteCreated, NoteTitleChanged。要實現事件響應,咱們能夠經過實現框架提供的IEventHandler<T>接口,就能告訴框架,我要訂閱什麼事件了。分佈式

上面代碼中,應該比較詳細的註釋了每段代碼的含義了,應該都能看懂吧。上面這個例子說明了,聚合跟本身的狀態不是在public方法中直接改的,而是基於事件驅動的方式來修改的,因此,你們能夠看到,聚合根狀態的修改是在一個內部響應函數中修改的。下面咱們再來看一下外部其餘對象,如何響應該事件:

    //這是一個事件訂閱者,它也響應了Note的兩個事件
    public class NoteEventHandler :
        IEventHandler<NoteCreated>,
        IEventHandler<NoteTitleChanged>
    {
        public void Handle(NoteCreated evnt)
        {
            //這裏爲了簡單,因此只是輸出了一串文字,實際咱們能夠在這裏作任何你想作的事情;
            Console.WriteLine(string.Format("Note created, title:{0}", evnt.Title));
        }
        public void Handle(NoteTitleChanged evnt)
        {
            Console.WriteLine(string.Format("Note title changed, title:{0}", evnt.Title));
        }
    }

經過上面兩個簡單的例子,不知道有沒有解釋清楚,在enode框架中,如何體現EDA?

總結:

我之因此比較喜歡事件驅動這種思想是基於如下理由:

  1. 就是上面我說的解耦+可擴展;
  2. 事件能夠並行執行;就是說,一個系統中,同時能夠有不少事件在並行的產生、傳遞、響應;這樣說,你們可能還理解不了這一點的價值。我說一下併發的概念。一般咱們所說的一個網站的併發,好比有5000,是指一個網站在1秒內的全部併發請求數,這麼多併發請求數是針對系統中全部的聚合根的;也就是若是平攤到每一個聚合根,那併發修改數通常就很低了,好比每秒只有10個併發,甚至只有1個或兩個。這點每一個系統有所不一樣,好比淘寶的商品秒殺活動,那當秒殺開始的時刻,對同一個商品的下單的併發數很高,由於每一個商品的每一個訂單都意味着要減庫存,因此這個減庫存的併發操做必定很高,實現起來確定很困難了,不經過可靠的分佈式緩存以及樂觀鎖機制,估計很難實現;而好比新浪微博上,咱們每一個人發微博,雖然整個新浪微博網站的總體併發數很高,由於確定每秒有很是多的人在寫微博,可是咱們同時也知道,你們寫的微博都是獨立的,沒有共享資源,每發表一條微博實際上就是建立一條數據庫記錄而已。因此能夠理解爲,單個對象無併發;而通常的企業應用或通常的互聯網應用,針對同一資源(同一個聚合根)的併發修改,通常都不高;因此基於這樣的分析和理解,咱們知道了,理論上,事件何時能夠並行產生和執行,何時必須排隊。就是:若是兩個事件不是同一個聚合根產生的,那就能夠並行處理,事件也能夠並行持久化;若是是單個聚合根產生的,那必須按照順序被持久化;因此,根據這樣的理解,咱們知道了,一個應用程序,除了單個聚合根上的修改只能串行進行外,其餘狀況理論上均可以並行執行;這段話說了這麼多關於併發數以及事件並行方面的東西,那究竟知道這些有什麼用呢?很簡單,只要和傳統的事務模式對比下就知道了,傳統的事務模式,若是要修改多個聚合根,那事務在執行的那一段時間,全部涉及到的聚合根都不能被其餘事務所修改;只有等到當前事務執行完成後,其餘事務才能執行;而經過事件的方式,因爲咱們沒有事務的概念,咱們惟一要確保的只是一個聚合根上產生的事件必須被一個個按順序持久化,這點咱們很簡單,好比咱們只要建一個聯合主鍵:聚合根ID+事件版本號,而後作樂觀併發控制便可;因此,事件持久化時,排他的粒度比事務要小,這樣的好處是無阻塞;那麼換來的好處就是網站總體的可用性高;可是帶來的壞處是,可能有可能會出現樂觀併發衝突,但這點咱們能夠經過框架的自動重試功能解決掉;並且,咱們也剛分析過,同一個聚合根的併發修改通常是很低的;因此經過事件的方式來達到這種細粒度的對聚合根的修改是很是有意義的。
  3. 配合Event Sourcing模式,可讓EDA發揮更大的價值,更準確的說,咱們可讓事件發揮更大的價值;就是:咱們不只可讓事件做爲消息,在系統各個對象或組件甚至是各個系統之間傳遞,還能夠用事件來還原整個系統的狀態。這點我會在後面詳細介紹enode框架中如何使用event sourcing這種模式;
相關文章
相關標籤/搜索