ENode框架Conference案例分析系列之 - Quick Start


前言

前一篇文章介紹了Conference案例的架構設計,本篇文章開始介紹Conference案例的代碼實現。由於代碼比較多,一開始就全部介紹所有細節,估計很多人接受不了,也理解不了。所以,我先進行一次QuickStart的介紹,即選取某個簡單典型的場景從前到后過一下每個環節。這樣大家就能夠快速對代碼的重要關鍵環節有大概的理解。另外,我現在正在做ENode的官網,到時會像axon framework一樣,介紹ENode框架本身、使用場景、性能數據、案例,以及論壇社區等功能;

本文打算選擇Conference案例中一個不太復雜的場景(發布會議),來快速過一下需要開發者實現的代碼環節。

UI

首先,我們來看一下發布一個會議的UI入口,前面的文章介紹過,當客戶創建好一個會議后,他可以先編輯會議的所有座位類型,然后如果允許預訂者預定了,那他就需要先發布這個會議。就像淘寶的賣家要上架商品后商品才會對買家可見一樣。本質上,發布一個會議,其實就是將會議聚合根的isPublished修改為true。UI如下圖所示:

Controller

當客戶點擊Publish按鈕后,前台提交HttpPost請求到服務端,然后請求就會被ASP.NET MVC的Controller處理,Controller的Action的邏輯如下:

上面的代碼比較清晰,我們先判斷當前的_conference實例是否為空,如果為空,則直接返回HttpNotFound結果。那_conference實例哪里來的?這里考慮到ConferenceController中的大部分Action都要使用當前的_conference實例,所以我們為了代碼的復用,在Controller的OnActionExecuting方法里,提前獲取了當前的_conference實例,代碼我稍后再貼。_conference實例有了之后我們就可以構建一個PublishConference的命令。該命令只需要一個當前要發布的Conference的ID即可。然后,我們調用ExecuteCommandAsync方法,去異步執行一個命令。然后,我們使用await關鍵字異步等待命令的處理結果。最后判斷結果是否成功,做相應的處理。

ExecuteCommandAsync方法:

該方法內部使用_commandService的ExecuteAsync方法來異步執行一個命令。_commandService是ENode框架提供的分布式的命令發送或執行服務,該服務是通過ConferenceController的構造函數注入的,代碼如下:

使用ENode框架開發的Controller,一般是需要兩個服務,一個是commanService,用於發送命令;另一個是某個queryService,用語查詢數據;Controller依賴這兩個服務充分體現了CQRS架構的特點。當然,有時查詢服務可能不止一個,那就可以注入多個,看我們自己需要即可。ENode使用的依賴注入框架是Autofac。大家可能在想,為何要弄一個ExecuteCommandAsync方法出來呢?因為要處理超時的情況,假如一個命令處理超時了(比如5s),那Controller的Action也需要立即返回了。TimeoutAfter的代碼如下:

大家可以看到TimeoutAfter方法內部,為了實現當超過指定時間后要求的Task還未處理完的情況,我們創建了一個延后指定時間執行的Task,然后通過Task.WhenAny方法異步等待任務執行,最后判斷完成的Task是哪個,從而實現超時的處理。這個做法是我在網上找到的,覺得還不錯,這個做法可以讓我們在實現完全異步的同時還能實現超時處理。最后,我們看一下OnActionExecuting方法:

OnActionExecuting

這個方法的代碼的邏輯也比較簡單,就是根據HttpRequest中包含的slug參數先獲取一個Conference聚合根;如果存在,則進一步根據accessCode參數檢查accessCode參數是否合法。通過合法,則認為提供的slug和accessCode有效。大家可以把slug理解為唯一定位一個Conference的,而accessCode是使用該Conference的密碼。由於這個只是一個案例,所以我們通過這種簡單有效的方法來為用戶授權。

Command

了解了Controller的實現,我們接下來看看Command的定義,Command是一個DTO對象。代碼如下:

非常簡單,由於ENode框架基類提供的Command類已經提供了一個AggregateRootId的屬性,所以我們的PublishConference命令無需再定義其他額外的屬性了。需要提一下的是,ENode框架要求,所有Command要創建或修改的聚合根的ID必須在Command發送之前賦值,這個是框架的一個約束,我認為這個通常不是問題。如果你希望聚合根的ID是一個long,那也許你需要自己部署一個全局long生成服務了,有興趣的朋友可以和我交流實現方案,我有實現經驗。如果你的ID是一個字符串,那用ENode框架提供的ObjectId類即可,它可以幫你自動生成一個24位長度的全局唯一順序字符串。接下來我們看看Command Handler的實現。

Command Handler

一個CommandHandler中的代碼通常是一句話,ENode框架的最大好處是可以讓開發者無需關注C端的技術實現,開發者只需要關心如何實現自己的業務邏輯即可。如上圖所示,我們會先定義一個ConferenceCommandHandler的類,然后實現ICommandHandler<PublishConference>接口,然后進一步實現對應的Handle方法。在Handle方法內部,我們只需要從當前的上下文根據Command所關聯的聚合根ID獲取當前要操作的聚合根,然后調用聚合根的業務方法即可。我們不需要像經典DDD那樣把聚合根從IConferenceRepository中取出來,再修改聚合根,再保存聚合根。並且經典DDD往往還會和工作單元(Unit of Work)配合;因為經典DDD,是支持一個應用層的方法同時修改多個聚合根的,而ENode框架是要求一個命令一次只能創建或修改一個聚合根,即架構設計上就是面向最終一致性的,主要目的為了實現更高的吞吐量,這點開發者需要明確與了解。CommandHandler,從代碼實現的角度,我相信ENode框架提供的方式是非常簡單和直接的,沒有任何多余的東西。大家可以看到使用ENode框架開發,大部分情況是不需要定義Repository的。下面我們來看看Domain聚合根的實現。

Domain & Domain Event

使用ENode框架開發的領域模型,聚合根的實現通常是這樣的:

  1. 繼承基類AggregateRoot<TAggregateRootId>
  2. 構造函數要傳給基類聚合根的ID
  3. 然后聚合根自己會提供一些可以修改自己狀態的公共方法,例如上面的Publish,Unpublish方法
  4. 聚合根內部會有一些私有的Handle方法,這些Handle方法是根據對應的事件,更新自己的狀態(事件驅動聚合根狀態的修改)

當前我們這里被調用的方法是Publish,該方法內部,先判斷當前聚合根是否已經處於發布狀態,如果是,則拋出異常即可,當然,你選擇忽略也沒問題;如果不是,則調用ApplyEvent方法Apply當前領域事件。ApplyEvent方法的邏輯是,先找到當前事件對應的Handle方法,然后調用該Handle方法;然后調用完成后,把當前事件放入一個聚合根內部的事件隊列中。

如果對ENode框架的實現有一定了解的朋友應該知道,ENode在處理一個命令時,ENode框架處理Command的核心流程是這樣的:

  1. 先創建一個空的ICommandContext對象;
  2. 調用CommandHandler的Handle方法,並把ICommandContext傳給Handle方法;
  3. 當Handle方法結束后,ENode框架就能知道當前ICommandContext中有哪些聚合根修改了或創建了(框架要求一個命令一次只能涉及一個聚合根的修改)然后框架如果拿到了某個修改的聚合根,它就拿出該聚合根里上面提到的內部的事件隊列里的事件。然后根據這些事件生成一個EventStream的對象;
  4. 持久化EventStream對象到EventStore;
  5. 發布(Publish)EventStream到EQueue,然后外部的Event Handler就都能響應領域事件了;

上面這個是正常流程,在這里我順便提一下,為了讓大家更好的理解內部實現的機制。通過上面這些介紹,我想大家應該至少可以理解上面的Publish方法和Handle方法了吧。

另外,有些朋友可能會想,為何是先產生事件,再修改狀態呢?

主要原因是因為這個Handle方法是會在事件溯源(ES)的時候被重復利用的。當我們要從EventStore通過ES還原某個聚合根時,我們是先從EventStore獲取該聚合根所產生的所有的事件,然后對每個事件調用聚合根的對應的Handle方法,從而實現聚合根狀態的還原。這個過程也就是我們常說的事件溯源,即ES(Event Sourcing)。

需要強調的是,聚合根應該在產生事件之前把各種業務規則和業務邏輯實現掉,然后只有當前操作滿足所有的業務規則時,才調用ApplyEvent方法。然后在聚合根里的所有Handle方法中,就是僅僅簡單的等於號賦值操作,不能有任何業務邏輯,這點非常重要。為什么要這樣呢?因為假如我們把一些業務規則和邏輯放在Handle方法中,比如if怎么樣的時候做什么賦值,else的時候做另外的賦值。那假如哪一天我們的Handle方法里的判斷邏輯變化了,那我們通過事件溯源還原出來的聚合根的狀態就不對了。這點應該不難理解吧。

從更高層面(哲學)的角度來理解,EventStore中存儲的事件並不是完整的歷史。事件+聚合根的Handle方法才是完整的歷史,兩者結合才可以完整地將聚合根的狀態還原到最新狀態。因為是歷史,歷史無法改變,所以我們的事件和Handle方法也都不能修改;或者如果真的要修改,也必須確保兼容老的結構和實現,這點非常重要。下面我們來看看Event Handler的實現:

Event Handler

EventHandler的作用是根據C端聚合根產生的事件來更新CQRS的讀庫。需要注意的是ENode整個框架對外提供的API基本都是異步IO的(實際上內部的實現也都是異步IO的,只有整個鏈路都是異步得,才能發揮異步的好處)。所以我們更新讀庫時,需要使用ADO.NET提供的Async方法類更新DB。這里我使用ENode自帶的Dapper輕量級高性能ORM來實現對讀庫的更新。上面的代碼中就是更新Conference表的IsPublish字段。但是為了確保避免並發導致的數據覆蓋,所以我們需要嚴謹的利用樂觀控制來確保數據不會被覆蓋,ENode要求我們使用Version機制來實現樂觀鎖。

關於並發控制的討論,其實還有非常多的細節可以討論。我之前寫過一篇文章,大家有興趣的可以去看一下,本文的目的是做一個QuickStart,所以不做過多展開了。TryUpdateRecordAsync方法的內部實現如下,很簡單,我就不做介紹了。

還有一點需要特別提一下,就是為何要使用Dapper而不使用EF這種ORM框架。因為ENode框架實現的是CQRS+ES的架構。所以,我們在更新讀庫時,是根據事件更新讀庫。那怎么樣的更新是最快的呢?就是直接通過Insert或Update語句來更新DB。而如果通過EF這種框架,因為是面向OO的ORM,所以一般是需要先從DB取出數據轉換為對象,再更新對象,再保存對象這樣的思路。這個過程我個人認為,對於CQRS+ES架構的應用來說,是比較繁瑣和低效(2次IO,先讀出來,再保存回去)的。我們在更新讀庫時,更好的方式應該是利用像Dapper這樣的ORM框架,簡單直接的更新讀庫(一次IO操作即可)。另外,我通過對Dapper做了一些簡單的二次封裝,可以做到用最直接的代碼實現目的,且兼顧了代碼的可讀性、可維護性、靈活性,以及性能。另外,查詢數據時,通過Dapper也非常簡單,而且還支持返回dynamic對象。Dapper是基於約定的框架,不需要做ORM映射方面的配置。我個人認為使用在CQRS+ES架構中是非常合理的。所以,對我來說,EF可以退休了,呵呵。

總結

好了,上面介紹了發布會議的所有需要用戶寫的代碼,是不是很簡單呢?我個人認為和經典DDD的架構相比,由於有ENode框架的支持,所以開發基於CQRS+ES架構的應用,是非常簡單的。下一篇要寫什么還沒想好,大家還想了解什么,可以及時給我反饋啊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM