CQRS之旅——旅程5(準備發佈V1版本)

来源:https://www.cnblogs.com/angrymoto/archive/2019/06/20/cqrs-journey-day5.html
-Advertisement-
Play Games

本章描述了團隊為準備Contoso會議管理系統的第一個產品版本所做的更改。這項工作包括對前兩章介紹的訂單(Order)和註冊(Registrations)限界上下文的一些重構和功能添加,以及一個新的會議管理(Conference Management)限界上下文和一個新的支付(Payment)限界上... ...


旅程5:準備發佈V1版本

添加功能和重構,為V1版本發佈做準備。

“大多數人在完成一件事之後,就像留聲機的唱片一樣,一遍又一遍地使用它,直到它破碎,忘記了過去是用來創造更多未來的東西。” -- 弗雷婭.斯塔克   

發佈Contoso會議管理系統V1版本:

本章描述了團隊為準備Contoso會議管理系統的第一個產品版本所做的更改。這項工作包括對前兩章介紹的訂單(Order)和註冊(Registrations)限界上下文的一些重構和功能添加,以及一個新的會議管理(Conference Management)限界上下文和一個新的支付(Payment)限界上下文。

團隊在此過程中進行的一個關鍵重構是將事件源(ES)引入訂單(Order)和註冊(Registrations)限界上下文中。

實現CQRS模式的一個預期好處是,它將幫助我們在複雜系統中管理變化。在CQRS旅程中發佈一個V1版本將幫助團隊評估當我們從V1版本遷移到系統的下一個產品版本時使用CQRS和ES的好處。剩下的章節將描述V1版本發佈後的情況。

本章描述了團隊在此階段添加到公共網站的用戶界面(UI),並包括了對基於任務的UI的討論。

本章的工作術語定義:

本章使用了一些術語,我們將在下麵進行描述。有關更多細節和可能的替代定義,請參閱參考指南中的“深入CQRS和ES”。

  • Access code(訪問代碼):當業務客戶創建一個新的會議時,系統生成一個5個字元的訪問代碼並通過電子郵件發送給業務客戶。業務客戶可以使用其電子郵件地址和會議管理網站上的訪問代碼在稍後的日子從系統中檢索會議詳細信息。該系統使用訪問碼而不是密碼,因此業務客戶不需要僅為了創建一個支付而註冊賬戶。

  • Event sourcing(事件源):事件源是在系統中持久化和重新載入聚合狀態的一種方法。每當聚合的狀態發生更改時,聚合將引發詳細說明狀態更改的事件。然後,系統將此事件保存到事件存儲中。系統可以通過重播與聚合實例關聯的所有先前保存的事件來重新創建聚合的狀態。事件存儲成為系統存儲數據的記錄簿。此外,您還可以使用事件源作為審計數據的來源,作為查詢歷史狀態、從過去的數據獲得新的業務見解以及重播事件以進行調試和問題分析的方法。

  • Eventual consistency(最終一致性):最終一致性是一個一致性模型,它不能保證立即訪問更新的值。對數據對象進行更新後,存儲系統不保證對該對象的後續訪問將返回更新後的值。然而,存儲系統確實保證,如果在足夠長的時間內沒有對對象進行新的更新,那麼最終所有訪問都可以返回最後更新的值。

用戶故事(User stories)

在這個過程的這個階段,團隊實現了下麵描述的用戶故事。

定義通用語言

  • 業務客戶:業務客戶代表使用會議管理系統運行其會議的組織。

  • 座位:座位代表會議上的一個空間或進入會議上特定會議如歡迎招待會、教程或研討會的通道。

  • 註冊者:註冊者是與系統交互下訂單併為這些訂單付款的人。註冊者還創建與訂單關聯的註冊。

會議管理限界界的上下文的用戶故事

業務客戶可以創建新的會議並管理它們。在業務客戶創建新會議之後,他可以使用電子郵件地址和會議定位器訪問代碼訪問會議的詳細信息。當業務客戶創建會議時,系統生成訪問代碼。

業務客戶可以指定以下關於會議的信息:

  • 名稱、描述和Slug(用於訪問會議的URL的一部分)。
  • 會議的開始和結束日期。
  • 會議提供的不同座位類型和配額。

此外,業務客戶可以通過發佈或取消發佈會議來控制會議在公共網站上的可見性。

業務客戶可以使用會議管理網站查看訂單和與會者列表。

訂單和註冊限界的上下文的用戶故事

當註冊者創建一個訂單時,可能無法完全完成該訂單。例如,註冊者申請5個座位參加整個會議,5個座位參加歡迎招待會,3個座位參加會前講習班。整個會議可能只有3個座位,歡迎招待會只有1個座位,但會前講習班有3個以上的座位。系統會將此信息顯示給註冊者,並讓她有機會在繼續付款過程之前按順序調整每種座位的數量。

當註冊者選擇了每種座位類型的數量後,系統會計算訂單的總價,然後註冊者可以使用線上支付服務支付這些座位。Contoso不代表客戶處理付款。每個業務客戶必須有一個通過線上支付服務接受支付的機制。在項目的後期,Contoso將添加對業務客戶的支持,以將他們的發票系統與會議管理系統集成在一起。在將來的某個時候,Contoso可能會提供一項代表客戶收款的服務。

備註:在系統的這個版本中,實際上支付系統是模擬的。

註冊者在會議上購買了座位後,可以為參會者分配這些座位。系統存儲每個參會者的姓名和聯繫方式。

架構

下圖說明瞭在V1版本中Contoso會議管理系統的關鍵體系架構。該應用程式由兩個網站和三個限界上下文組成。基礎設施包括Microsoft Azure SQL資料庫(SQL Database)實例、事件存儲和消息傳遞基礎設施。

圖後面的表列出了圖中顯示的構件(聚合、MVC控制器、讀取模型生成器和數據訪問對象)相互交換的所有消息。

備註:為了清晰,圖中沒有展示Handlers(把消息發送給領域對象的類,例如:OrderCommandHandler)。

元素 類型 發送 接收
ConferenceController MVC Controller N/A ConferenceDetails
OrderController MVC Controller AssignSeat
UnassignSeat
DraftOrder
OrderSeats
PricedOrder
RegistrationController MVC Controller RegisterToConference
AssignRegistrantDetails
InitiateThirdPartyProcessorPayment
DraftOrder
PricedOrder
SeatType
PaymentController MVC Controller CompleteThirdPartyProcessorPayment
CancelThirdPartyProcessorPayment
ThirdPartyProcessorPaymentDetails
Conference Management CRUD Bounded Context ConferenceCreated
ConferenceUpdated
ConferencePublished
ConferenceUnpublished
SeatCreated
SeatUpdated
OrderPlaced
OrderRegistrantAssigned
OrderTotalsCalculated
OrderPaymentConfirmed
SeatAssigned
SeatAssignmentUpdated
SeatUnassigned
Order Aggregate OrderPlaced
OrderExpired
OrderUpdated
OrderPartiallyReserved
OrderReservationCompleted
OrderPaymentConfirmed
OrderRegistrantAssigned
RegisterToConference
MarkSeatsAsReserved
RejectOrder
AssignRegistrantDetails
ConfirmOrderPayment
SeatsAvailability Aggregate SeatsReserved
AvailableSeatsChanged
SeatsReservationCommitted
*SeatsReservationCancelled
MakeSeatReservation
CancelSeatReservation
CommitSeatReservation
AddSeats
RemoveSeats
SeatAssignments Aggregate SeatAssignmentsCreated
SeatAssigned
SeatUnassigned
SeatAssignmentUpdated
AssignSeat
UnassignSeat
RegistrationProcessManager Process manager MakeSeatReservation
ExpireRegistrationProcess
MarkSeatsAsReserved
CancelSeatReservation
RejectOrder
CommitSeatReservation
ConfirmOrderPayment
OrderPlaced
PaymentCompleted
SeatsReserved
ExpireRegistrationProcess
OrderViewModelGenerator Handler DraftOrder OrderPlaced
OrderUpdated
OrderPartiallyReserved
OrderReservationCompleted
OrderRegistrantAssigned
PricedOrderViewModelGenerator Handler N/A SeatTypeName
ConferenceViewModelGenerator Handler Conference
AddSeats
RemoveSeats
ConferenceCreated
ConferenceUpdated
ConferencePublished
ConferenceUnpublished
SeatCreated
SeatUpdated
ThirdPartyProcessorPayment Aggregate PaymentCompleted
PaymentRejected
PaymentInitiated
InitiateThirdPartyProcessorPayment
CompleteThirdPartyProcessorPayment
CancelThirdPartyProcessorPayment

標記*的這些事件僅用於使用事件源持久化聚合狀態。

標記**的是ConferenceViewModelGenerator從SeatCreated和SeatUpdated事件創建的這些命令,這些事件在會議管理限界上下文中處理。

下麵的列表概述了Contoso會議管理系統中的消息命名約定

  • 所有事件在命名約定中都使用過去時。
  • 所有命令都使用命令式命名約定。
  • 所有的DTO都是名詞。

該應用程式旨在部署到Microsoft Azure。在旅程的那個階段,應用程式由兩個角色組成,一個包含ASP.Net MVC Web應用程式的web角色和一個包含消息處理程式和領域對象的工作角色。應用程式在寫端和讀端都使用Azure SQL DataBase實例進行數據存儲。訂單(Order)和註冊(Registrations)限界上下文現在使用事件存儲在寫端持久化狀態。此事件存儲是使用Azure table storage來實現的。應用程式使用Azure服務匯流排來提供其消息傳遞基礎設施。

在研究和測試解決方案時,可以在本地運行它,可以使用Azure compute emulator,也可以直接運行MVC web應用程式,並運行承載消息處理程式和領域域對象的控制台應用程式。在本地運行應用程式時,可以使用本地SQL Server Express資料庫,使用在SQL Server Express資料庫實現的簡單的消息傳遞基礎設施和簡單事件存儲。

備註:事件存儲和消息傳遞基礎設施的基於sql的實現只是為了幫助您在本地運行應用程式以進行探索和測試。它們並不是想要說明一種用於實際產品的方法。

有關運行應用程式的選項的更多信息,請參見附錄1“發佈說明”。

會議管理有界上下文

會議管理限界上下文是一個簡單的兩層,創建/讀取/更新(CRUD)風格的web應用程式。它使用ASP.NET MVC和Entity Framework。

這個限界上下文必須與實現CQRS模式的其他限界上下文集成。

模式和概念

本節介紹了在團隊旅程的當前階段,應用程式的一些關鍵地方,並介紹了團隊在處理這些地方時遇到的一些挑戰。

事件源

Contoso的團隊最初在沒有使用事件源的情況下實現了訂單和註冊的限界上下文。然而,在實現過程中,很明顯,使用事件源將有助於簡化這個限界上下文。

在第4章“擴展和增強訂單和註冊邊界上下文”中,團隊發現我們需要使用事件將更改從寫端推到讀端。在讀端,OrderViewModelGenerator類訂閱Order聚合發佈的事件,並使用這些事件更新由讀取模型查詢的資料庫中的視圖。

這已經是事件源實現的一半了,因此在整個限界上下文中使用基於事件的單一持久性機制是有意義的。

事件源基礎設施可在其他限界上下文中重用,訂單和註冊的實現也變得更加簡單。

Poe(IT運維人員)發言:

作為一個實際的問題,在V1發佈之前,團隊只有有限的時間來實現一個產品級別的事件存儲。他們基於Azure表創建了一個簡單的基本事件存儲作為臨時解決方案。但是,在將來從一個事件存儲遷移到另一個事件存儲時,他們可能會面臨問題。

關鍵是演進:例如,可以展示如何實現事件源使您擺脫那些冗長的數據遷移,甚至允許您從過去構建報告。

  • tom Janssens - CQRS Advisors郵件列表

團隊使用Azure表存儲實現了基本的事件存儲。如果您將應用程式托管在Azure中,還可以考慮使用Azure blobs或SQL資料庫來存儲事件。

在為事件存儲選擇基礎技術時,應該確保您的選擇能夠提供應用程式所需的可用性、一致性、可靠性、可伸縮性和性能需要。

Jana(軟體架構師)發言:

在選擇Azure中的存儲機制時要考慮的問題之一是成本。如果使用SQL資料庫,則根據資料庫的大小進行計費。如果使用Azure table或blob存儲,則根據使用的存儲量和存儲事務的數量進行計費。您需要仔細評估系統中不同聚合上的使用模式,以確定哪種存儲機制的成本效率最高。可能會發現,不同的存儲機制對於不同的聚合類型是有意義的。您可以引入一些優化來降低成本,例如使用緩存來減少存儲事務的數量。

根據我的經驗,如果您正在進行新手開發,那麼您需要非常好的辯論來選擇一種SQL資料庫。Azure存儲服務應該是預設的選擇。但是,如果您已經有一個想要遷移到雲中的SQL Server資料庫,那麼情況就不同了。

  • mark Seemann - CQRS Advisors郵件列表

確定聚合

在團隊為V1版本創建的基於Azure表存儲的事件存儲實現中,我們使用聚合ID作為分區鍵。這使得定位包含任何特定聚合事件的分區非常有效。

在某些情況下,系統必須定位相關的聚合。例如,訂單聚合可能具有相關的註冊聚合,其中包含分配到特定座位的參會者的詳細信息。在這個場景中,團隊決定為相關的聚合對(訂單和註冊聚合)重用相同的聚合ID,以便於查找。

Gary(CQRS專家)發言:

在這種情況下,您需要考慮是否應該有兩個聚合。您可以將註冊建模為訂單聚合內的實體。

更常見的場景是聚合之間存在一對多的關係,而不是一對一的關係。在這種情況下,不可能共用聚合ID,相反,“一”的聚合可以存儲“多”聚合的ID列表,而“多”的每個聚合可以存儲“一”聚合的ID。

當聚合存在於不同的限界上下文中時,共用聚合ID是很常見的。如果您在不同的限界上下文中使用聚合對同一個現實實體的不同方面建模,那麼它們共用相同的ID是有意義的。
Greg Young --與模式和實踐團隊的對話

基於任務的用戶界面

UI的設計在過去的十年中有了很大的改進。應用程式比以前更容易使用,更直觀,導航也更簡單。一些UI設計指南的例子可以幫助您創建這樣的現代的、用戶友好的應用程式,如Microsoft Inductive User Interface GuidelinesIndex of UX guidelines

影響UI設計和可用性的一個重要因素是UI如何與應用程式的其他部分通信。如果應用程式基於CRUD風格的體繫結構,這可能會泄漏到UI。如果開發人員專註於CRUD風格的操作,這可能會導致出現類似下圖(左邊)中第一個屏幕設計所示的UI。

在第一個屏幕上,按鈕上的文字反映了當用戶單擊Submit按鈕時系統將執行的底層CRUD操作,而不是顯示用戶更關心的操作的文字。不幸的是,第一個屏幕還要求用戶推理一些關於屏幕和應用程式功能的知識。例如,Add按鈕的功能並不是立即可見的。

第一個屏幕背後的典型實現將使用數據傳輸對象(DTO)在後端和UI之間交換數據。UI從後端請求數據,這些數據封裝在DTO中,UI將修改數據,然後將DTO發回到後端。後端將使用DTO來確定它必須對底層數據存儲執行哪些CRUD操作。

第二個屏幕更明確地顯示了業務流程方面正在發生的事情:用戶正在選擇座位類型的數量作為會議註冊任務的一部分。根據用戶正在執行的任務來考慮UI,可以更容易地將UI與CQRS模式實現中的寫模型關聯起來。UI可以向寫端發送命令,這些命令是寫端領域模型的一部分。在實現CQRS模式的限界上下文中,UI通常查詢讀端並接收DTO,然後向寫端發送命令。

上圖顯示了一系列頁面,這些頁面使註冊者能夠完成“在會議上購買座位”的任務。在第一頁,註冊者選擇座位的類型和數量。在第二頁,註冊者可以查看她所預訂的座位,輸入她的聯繫方式,並完成必要的付款信息。然後系統將註冊者重定向到支付提供者,如果支付成功完成,系統將顯示第三個頁面。第三個頁面顯示了訂單的摘要,並提供了到註冊者可以啟動其他任務的頁面的鏈接。

為了突出顯示基於任務的UI中命令和查詢的角色,故意簡化了上圖中所示的序列。例如,實際流程包括系統根據註冊者選擇的支付類型顯示的頁面,以及如果支付失敗系統顯示的錯誤頁面。

Gary(CQRS專家)發言:

您並不總是需要使用基於任務的UI。在某些場景中,簡單的CRUD風格的UI工作得很好。您必須評估基於任務的UI的好處是否大於所需的額外實現工作。通常,選擇實現CQRS模式的限界上下文也是受益於基於任務的UI的限界上下文,因為它們具有更複雜的業務邏輯和更複雜的用戶交互。

我想一勞永逸地聲明,CQRS不需要基於任務的UI。我們可以將CQRS應用於基於CRUD的介面(儘管創建分離的數據模型之類的事情要困難得多)。
然而,有一件事確實需要基於任務的UI。這就是領域驅動設計。
-Greg Young, CQRS, Task Based UIs, Event Sourcing agh!

更多信息,請參見參考指南中的第4章“深入CQRS和ES”。

CRUD

您不應該將CQRS模式用作頂層體繫結構的一部分。您應該只在模式帶來明顯好處的限界上下文中實現模式。在Contoso會議管理系統中,會議管理限界上下文是整個系統中相對簡單、穩定和低容量的一部分。因此,團隊決定使用傳統的兩層CRUD風格的體繫結構來實現這個限界上下文。

有關CRUD風格的體繫結構何時適合(或不適合)的討論,請參閱博客文章:Why CRUD might be what they want, but may not be what they need

限界上下文之間的集成

會議管理限界上下文需要與訂單和註冊限界上下文集成。例如,如果業務客戶更改會議管理限界上下文中座位類型的配額,則必須將此更改傳播到訂單和註冊限界上下文中。此外,如果註冊者向會議添加了一個新的參會者,業務客戶必須能夠在會議管理網站的列表中查看到參會者的詳細信息。

從會議管理限界上下文中推送更新

下麵是幾位開發人員和領域專家之間的對話,這些對話強調了團隊在計劃如何實現此集成時需要解決的一些關鍵問題。

  • 開發人員1:我想談談我們應該如何實現與我們CRUD風格的會議管理限界上下文相關聯的集成任務的兩部分。首先,當業務客戶在此限界上下文中為現有會議創建新會議或定義新座位類型時,其他限界上下文中(如訂單和註冊限界上下文中)需要知道更改。其次,當業務客戶更改座位類型的配額時,其他限界上下文也需要知道這種更改。
  • 開發人員2:所以在這兩種情況下,您都會從會議管理有限上下文中推送更改。這是一個方法。
  • 開發人員1:是的。
  • 開發人員2:您所說的場景之間有什麼顯著的區別嗎?
  • 開發人員1:在第一個場景中,這些更改相對較少,通常在業務客戶創建會議時發生。此外,這些都僅是追加的更改。我們不允許業務客戶在會議首次發佈後刪除會議或座位類型。在第二種場景中,更改可能更頻繁,業務客戶可能會增加或減少座位配額。
  • 開發人員2:對於這些集成場景,您考慮哪些實現方法?
  • 開發人員1:因為我們有一個兩層的CRUD樣式的限界上下文,對於第一個場景,我計劃將會議和座位類型的信息直接從資料庫中公開成一個簡單的只讀服務。對於第二個場景,我計劃在業務客戶更新座位配額時發佈事件。
  • 開發人員2:為什麼這裡使用兩種不同的方法?使用單一的方法會更簡單。從長遠來看,使用事件更加靈活。如果其他限界上下文需要此信息,則可以輕鬆訂閱事件。使用事件可以減少限界上下文之間的耦合。
  • 開發人員1:我可以看到,如果我們使用事件,將更容易適應未來不斷變化的需求。例如,如果一個新的限界上下文需要知道關於誰更改了配額的信息,我們可以將此信息添加到事件中。對於現有的限界上下文,我們可以添加一個適配器,將新的事件格式轉換為舊的。
  • 開發人員2:您的意思是,通知訂閱者配額更改的事件發送的是配額的更改。例如,假設業務客戶將座位配額增加了50個。這樣訂閱者如果一開始沒有訂閱,就不能收到完整的更新曆史記錄,會發生什麼?
  • 開發人員1:我們可以包含一些使用當前狀態快照的同步機制。不管怎樣,在這種情況下,事件都可以簡單的報告配額的新值。如果有必要,事件可以報告座位配額的變化和當前值。
  • 開發人員2:如何確保一致性?您需要確保限界上下文將其數據持久存儲併在消息隊列上發佈事件。
  • 開發人員1:我們可以將資料庫寫操作和add-to-queue操作封裝在一個事務中。
  • 開發人員2:當網路數據的大小增加、響應時間變長和失敗的概率增加時,有兩個原因會導致以後出現問題。首先,我們的基礎設施使用Azure服務匯流排來處理消息。不能使用單個事務將服務匯流排上的消息發送和對資料庫的寫入結合起來。其次,我們試圖避免兩階段提交,因為從長遠來看,它們總是會導致問題。
  • 領域專家:我們有一個與另一個限界上下文類似的場景,我們將在稍後查看。在這種情況下,我們不能對限界上下文做任何更改,我們不再擁有源代碼的最新副本。
  • 開發人員1:我們可以做些什麼來避免使用兩階段提交?如果我們不能訪問源代碼,因此不能做任何更改,我們可以做什麼呢?
  • 開發人員2:在這兩種情況下,我們使用相同的技術來解決問題。我們可以使用另一個進程來監視資料庫,併在它檢測到資料庫中的更改時發送事件,而不是從應用程式代碼中發佈事件。這個解決方案可能會引入少量延遲,但是它確實避免了兩階段提交的需要,並且您可以在不更改應用程式代碼的情況下實現它。

另一個問題涉及何時何地持久化集成事件。在上面討論的示例中,會議管理限界上下文發佈事件,訂單和註冊限界上下文處理這些事件並使用它們填充其讀模型。如果發生了導致系統丟失讀模型數據的故障,那麼不保存事件就無法重新創建讀模型。

是否需要持久化這些集成事件將取決於應用程式的特定需求和實現。例如:

  • 寫端可以處理集成來替代在讀端處理,例如:事件將導致寫入端發生更改,這些更改將作為其他事件保存。
  • 集成事件可以當做臨時數據不做持久化。
  • 來自CRUD風格的限界上下文的集成事件可能包含狀態數據,因此只需要最後一個事件。例如,如果來自會議管理限界上下文的事件包含當前座位配額,您可能對以前的值不感興趣。

另一種要考慮的方法是使用多個限界上下文共用的事件存儲。這樣,原始的限界上下文(例如CRUD風格的會議管理限界上下文)可以負責持久化集成事件。

  • greg Young -與模式和實踐團隊的對話。

關於Azure服務匯流排的一些說明

前面的討論提出了一種在會議管理限界上下文中避免使用分散式兩段提交的方法。然而,也有其他的方法。

雖然Azure服務匯流排不支持分散式事務(把匯流排上的一個操作和資料庫上的一個操作合併),但您可以在發送消息時使用RequiresDuplicateDetection屬性,和在收到消息使用PeekLock模式。這樣可以創建出所需級別的健壯性而不使用分散式事務。

作為替代方案,您可以使用分散式事務來更新資料庫,並使用本地Microsoft消息隊列(MSMQ)發送消息。然後可以使用橋接器將MSMQ隊列連接到Azure服務匯流排隊列。

有關實現從MSMQ到Azure服務匯流排的橋接的示例,請參閱Microsoft Azure AppFabric SDK中的示例。

有關Azure服務匯流排的更多信息,請參見參考指南中的第7章“在參考實現中使用的技術”。

推送更改到會議管理限界上下文

將關於已完成訂單和註冊的信息從訂單和註冊限界上下文中推送到會議管理限界上下文中引發了一系列不同的問題。

訂單和註冊限界上下文通常在創建訂單時引發以下許多事件:OrderPlaced,OrderRegistrantAssigned,OrderTotalsCalculated,OrderPaymentConfirmed,SeatAssignmentsCreated,SeatAssignmentUpdated,SeatAssigned和 SeatUnassigned。限界上下文使用這些事件在聚合和事件源之間進行通信。

對於會議管理限界上下文來說,要捕獲顯示註冊和參會者詳細信息所需的信息,它必須處理所有這些事件。它可以使用這些事件包含的信息來創建數據的非規範化SQL表,然後業務客戶可以在UI中查看這些數據。

這種方法的問題是會議管理限界上下文需要從另一個限界上下文理解一組複雜的事件。這是一個脆弱的解決方案,因為訂單和註冊限界上下文的更改可能會破壞會議管理限界上下文中的這一特性。

Contoso計劃為系統的V1版本保留這個解決方案,但是將在旅程的下一階段評估其他方案。這些替代方案將包括:

  • 修改訂單和註冊限界上下文,以生成為集成而顯式設計的更有用的事件。
  • 在訂單和註冊限界上下文中生成非規範化數據,併在數據準備好時通知會議管理限界上下文。然後,會議管理有界上下文可以通過服務調用請求信息。

備註:要查看當前方法如何工作,請查看源代碼中Conference項目里的OrderEventHandler類。

選擇何時更新讀端數據

在會議管理有界上下文中,業務客戶可以更改座位類型的描述。這將引發一個SeatUpdated事件,由ConferenceViewModelGenerator類在訂單和註冊限界上下文中處理。該類更新讀模型數據,以反映有關座椅類型的新信息。當註冊者下訂單時,UI顯示新的座位描述。

然而,如果註冊者查看先前創建的訂單(例如為參會者分配座位),註冊者將看到原始的座位描述。

Carlos(領域專家)發言:

這是一個要反覆思考的商業決策。我們不想讓註冊者因為在創建訂單後更改座位描述而混淆。

Gary(CQRS專家)發言:

如果我們想要更新現有訂單上的座位描述,我們需要修改PricedOrderViewModelGenerator類來處理SeatUpdated事件並調整它的視圖模型。

分散式事務和事件源

上一節討論了會議管理限界上下文的集成選項,提出了使用分散式兩段提交事務的問題,以確保存儲會議管理數據的資料庫和向其他限界上下文發佈更改的消息傳遞基礎設施之間的一致性。

實現事件源時也會出現同樣的問題:必須確保存儲所有事件的限界上下文中的事件存儲與將這些事件發佈到其他限界上下文中的消息傳遞基礎設施之間的一致性。

事件存儲實現的一個關鍵特性應該是,它提供了一種方法來確保其存儲的事件與限界上下文發佈到其他限界上下文的事件之間的一致性。

Carlos(領域專家)發言:

如果您決定自己實現一個事件存儲,這是您應該解決的一個關鍵挑戰。如果您正在設計一個可伸縮的事件存儲,並計劃將其部署到分散式環境(如Azure)中,那麼您必須非常小心,以確保滿足這一需求。

自治和授權

訂單和註冊限界上下文負責代表註冊者創建和管理訂單。支付限界上下文負責管理與外部支付系統的交互,以便註冊者可以為他們訂購的座位付費。

當團隊檢查這兩個限界上下文的領域模型時,發現兩個上下文都不知道定價。訂單和註冊上下文創建了一個訂單,其中列出了註冊者請求的不同座位類型的數量。支付綁定上下文只是將總數傳遞給外部支付系統。在某個時候,系統需要在調用支付流程之前計算訂單的總數。

團隊考慮了兩種不同的方法來解決這個問題:支持自治和支持權威。

支持自治

自治方法將計算訂單總數的任務分配給訂單和註冊限界上下文。它在需要執行計算時不依賴於另一個限界上下文,因為它已經擁有了必要的數據。在過去的某個時候,它將從其他限界上下文(例如會議管理限界上下文)收集所需的定價信息並緩存它。

這種方法的優點是訂單和註冊限界上下文是自治的。它不依賴於另一個限界上下文或服務的可用性。

缺點是價格信息可能已經過時。業務客戶可能在會議管理限界上下文中更改了定價信息,但該更改可能尚未到達訂單和註冊有界上下文中。

支持授權

在這種方法中,計算訂單總數的系統部分在執行計算時從限界上下文中(例如會議管理限界上下文中)獲取定價信息。訂單和註冊限界上下文仍然可以執行計算,或者可以將計算委托給系統中的另一個限界上下文或服務。

這種方法的優點是,每當計算訂單總數時,系統總是使用最新的定價信息。

缺點是,當需要確定訂單總數時,訂單和註冊限界上下文依賴於另一個限界上下文。它要麼需要查詢會議管理限界上下文以獲得最新的定價信息,要麼調用另一個執行計算的服務。

**在自治和授權之間選擇

這兩種之間的選擇是一個業務決策。場景的特定業務需求決定採用哪種方法。自治通常是大型線上系統的首選。

Jana(軟體架構師)發言:

這個選擇可能會根據系統的狀態而改變。考慮一個超額預訂的場景。當大量會議席位仍然可用時,自治策略可能會在正常情況下進行優化,但是隨著特定會議的滿員,系統可能需要變得更加保守,並使用關於座位可用性的最新信息來支持授權。

會議管理系統計算訂單總數的方法是選擇自治而不是授權的一個例子。

Carlos(領域專家)發言:

對於Contoso來說,自治是明確的選擇。註冊者因為其他一些限界上下文掛了而不能購買座位是一個嚴重的問題。無論怎樣,我們並不真正關心業務客戶修改的定價信息和用於計算訂單總數的新定價信息之間是否存在短暫的延遲。

下麵的計算彙總部分描述了系統如何執行此計算。

讀端的實現方法

在前幾章對讀端進行的討論中,您看到了團隊如何使用基於sql的存儲來從寫端對數據進行非規範化的映射。

您可以為讀取模型數據使用其他存儲機制。例如,您可以使用文件系統或Azure table或blob來存儲。在訂單和註冊限界上下文中,系統使用Azure blob存儲關於座位分配的信息。

Gary(CQRS專家)發言:

當您為讀端選擇底層存儲機制時,除了要求讀端上的查詢方便且高效外,還應該考慮與存儲相關的成本(尤其是在雲中)。

備註:請參閱SeatAssignmentsViewModelGenerator類,以瞭解如何將數據持久化到blob存儲,以及SeatAssignmentsDao類,以瞭解UI如何檢索數據以供顯示。

最終一致性

在測試期間,團隊發現了一個場景,在這個場景中,註冊者可能會看到操作中最終一致性的證明。如果註冊者將參會者分配到訂單上購買的座位,然後快速導航到查看分配,那麼有時該視圖只顯示部分更新。然而,刷新頁面會顯示正確的信息。這是因為記錄座位分配的事件傳播到讀模型需要時間,有時測試人員會過早地查看從讀模型查詢的信息。

儘管生產系統更新讀取模型的速度可能比本地運行的應用程式的調試版本要快,但是團隊決定在視圖頁面中添加一個註釋,警告用戶這種可能性。

Carlos(領域專家)發言:

只要註冊者知道更改已經被持久化,並且UI顯示的內容可能過期了幾秒鐘,他們就不會擔心。

實現細節

本節描述訂單和註冊限界上下文的實現的一些重要功能。您可能會發現擁有一份代碼拷貝很有用,這樣您就可以繼續學習了。您可以從Download center下載一個副本,或者在GitHub上查看存儲庫:https://github.com/mspnp/cqrs-journey-code。您可以從GitHub上的Tags頁面下載V1發行版的代碼。

備註:不要期望代碼示例與參考實現中的代碼完全匹配。本章描述了CQRS過程中的一個步驟,隨著我們瞭解更多並重構代碼,實現可能會發生變化。

會議管理限界上下文

會議管理限界上下文允許業務客戶定義和管理會議,它是一個簡單的兩層、CRUD風格的應用程式,使用ASP.MVC。

在Visual Studio解決方案中,Conference項目包含模型代碼和Conference.Web項目。Conference.Web項目包含MVC View和Controller。

與訂單和註冊限界上下文進行集成

會議管理限界上下文通過發佈以下事件將更改通知推送到會議。

  • ConferenceCreated。在業務客戶創建新會議時發佈。
  • ConferenceUpdated。在業務客戶更新現有會議時發佈。
  • ConferencePublished。每當業務客戶發佈會議時發佈。
  • ConferenceUnpublished。每當業務客戶取消發佈新會議時發佈。
  • SeatCreated。每當業務客戶定義新座位類型時發佈。
  • SeatsAdded。每當業務客戶增加座位類型的配額時發佈。

Conference項目中的ConferenceService類將這些事件發佈到事件匯流排。

Markus(軟體開發人員)發言:

目前,還沒有分散式事務來把資料庫更新和消息發佈包裝到一起。

支付限界上下文

支付限界上下文負責和支付的外部系統交互,進行支付的處理和驗證。在V1版本中,支付可以通過模擬的外部第三方支付處理器(模仿PayPal等系統的行為)或發票系統進行處理。外部系統可以報告付款成功或失敗。

下圖中的序列圖演示了支付過程中涉及的關鍵元素如何相互交互。該圖顯示了一個簡化的視圖,忽略了處理程式類以更好地描述流程。

上圖顯示了訂單和註冊限界上下文、支付限界上下文和外部支付服務如何相互交互。在未來,註冊用戶也可以通過發票支付來替代第三方支付服務。

註冊者將支付作為UI中整個流程的一部分,如上圖所示。PaymentController控制器類先不顯示視圖,它必須等待系統創建第三方ThirdPartyProcessorPayment聚合實例。它的作用是將從註冊者收集的支付信息轉發給第三方支付處理程式。

通常,當您實現CQRS模式時,您使用事件作為限界上下文之間通信的機制。然而,在本例中,RegistrationControllerPaymentController控制器類向支付限界上下文發送命令。支付限界上下文使用事件與訂單和註冊限界上下文中的RegistrationProcessManager實例通信。

支付限界上下文的實現使用了CQRS模式,但沒有事件源。

寫端模型包含一個名為ThirdPartyProcessorPayment的聚合,它由兩個類組成:ThirdPartyProcessorPaymentThirdPartyProcessorPaymentItem。通過使用Entity Framework將這些類的實例持久化到SQL資料庫實例中。PaymentsDbContext類實現了一個Entity Framework dbcontext。

ThirdPartyProcessorPaymentCommandHandler是一個在寫端實現的命令處理程式。

讀取端模型也使用Entity Framework實現。PaymentDao類在讀端導出支付數據。請參見GetThirdPartyProcessorPaymentDetails方法。

下圖說明瞭組成支付限界上下文的讀端和寫端的不同部分。

與線上支付服務的集成、最終的一致性和命令驗證

通常,線上支付服務提供兩種級別的集成方式:

  • 簡單的方法是通過一種簡單的重定向機制來工作,您不需要與支付提供者建立一個商家帳戶。您將客戶重定向到支付服務。支付服務接受支付,然後將客戶重定向回網站上的一個頁面,並附帶一個確認碼。
  • 複雜的方法(您確實需要一個商家帳戶)是基於API的。它通常分兩步執行。首先,支付服務驗證您的客戶是否可以支付所需的金額,並向您發送一個令牌。其次,您可以在固定的時間內使用令牌,通過將令牌發送回支付服務來完成支付。

Contoso假定其業務客戶沒有商戶帳戶,必須使用簡單的方法。這樣做的一個後果是,在客戶完成付款時,座位預訂可能會過期。如果發生這種情況,系統會嘗試在客戶付款後重新獲得座位。如果無法重新獲得座位,系統會將此問題通知業務客戶,業務客戶必須手動解決此情況。

備註:該系統允許一點額外的時間,顯示在倒計時時鐘上,來完成支付過程。

在這個特定的場景中,如果沒有用戶(在本例中是業務所有者,他必鬚髮起退款或覆蓋座位配額)的手動干預,系統無法使自己完全一致,這說明瞭與最終一致性和命令驗證相關的以下更普遍的觀點。

接受最終一致性的一個關鍵好處是消除了使用分散式事務的需求,由於大型系統中必須持有的鎖的數量和持續時間,分散式事務對可伸縮性和性能有顯著的負面影響。在這個特定的場景中,您可以採取以下兩種方式來避免在沒有座位的情況下接受付款的潛在問題:

  • 把系統更改成在付款前重新檢查座位是否有空位。但這是不現實的,因為與支付系統的集成是在沒有商戶帳戶的情況下工作的。
  • 保留座位直到付款完畢。這也很困難,因為你不知道付款過程需要多長時間。您必須預留(鎖定)座位一段不確定的時間,等待註冊人完成付款。

團隊選擇允許這樣一種可能性,即註冊者可以付費購買座位,卻發現座位已不再可用。在實際中不太可能發生超時,除非註冊者要付費的座位很多。這種方法對系統的影響最小,因為它不需要對任何座位進行長期預訂(鎖定)。

Markus(軟體開發人員)發言:

為了進一步減少發生這種情況的機會,團隊決定將釋放預留座位的緩衝時間從5分鐘增加到14分鐘。選擇5分鐘的原始值是為了考慮伺服器之間任何可能的時鐘傾斜使得在UI中的15分鐘倒計時器過期之前不會釋放預訂。

在更通常的情況下,你可以重申上述兩個選項:

  • 在執行命令之前驗證命令,以確保命令成功。
  • 鎖定所有資源,直到命令完成。

如果命令隻影響單個聚合,並且不需要引用聚合定義的一致性邊界之外的任何內容,那麼就沒有問題,因為驗證命令所需的所有信息都在聚合中。目前的情況並非如此。如果您能在付款之前驗證座位是否仍然可用,那麼這個信息將需要檢查當前彙總之外的信息。

如果選擇驗證命令,您需要查看聚合之外的數據,例如,通過查詢讀模型或查看緩存,系統的可伸縮性將受到負面影響。另外,如果您正在查詢一個讀模型,請記住讀模型最終是一致的。在當前場景中,您需要查詢最終一致的讀模型來檢查座位的可用性。

如果您決定在命令完成之前鎖定所有相關資源,請註意這將對系統的可伸縮性造成的影響。

從業務角度處理這樣的問題要比在系統上設置大型架構約束好得多。
-- Greg Young
有關這個問題的詳細討論,請參閱Q/A Greg Young's Blog

事件源

事件源基礎設施的初始實現是非常基本的:團隊打算在不久的將來用產品質量的事件存儲來替換它。本節描述了初始的、基本的實現,併列出了改進它的各種方法。

這個基本事件源解決方案的核心要素是:

  • 每當聚合實例的狀態發生更改時,實例將引發一個事件,該事件將完整地描述狀態更改。
  • 系統將這些事件保存在事件存儲中。
  • 聚合可以通過重播其過去的事件流來重建狀態。
  • 其他聚合和流程管理器(可能在不同的限界上下文中)可以訂閱這些事件。

當聚合狀態發生更改時引發事件

訂單(Order)聚合中的以下兩個方法是OrderCommandHandler類在接收訂單命令時調用的方法的示例。這兩種方法都不會更新訂單(Order)聚合的狀態。相反,它們引發一個事件,該事件將由訂單(Order)聚合處理。在MarkAsReserved方法中,有一些最小的邏輯來確定要引發哪兩個事件。

public void MarkAsReserved(DateTime expirationDate, IEnumerable<SeatQuantity> reservedSeats)
{
    if (this.isConfirmed)
        throw new InvalidOperationException("Cannot modify a confirmed order.");

    var reserved = reservedSeats.ToList();

    // Is there an order item which didn't get an exact reservation?
    if (this.seats.Any(item => !reserved.Any(seat => seat.SeatType == item.SeatType && seat.Quantity == item.Quantity)))
    {
        this.Update(new OrderPartiallyReserved { ReservationExpiration = expirationDate, Seats = reserved.ToArray() });
    }
    else
    {
        this.Update(new OrderReservationCompleted { ReservationExpiration = expirationDate, Seats = reserved.ToArray() });
    }
}

public void ConfirmPayment()
{
    this.Update(new OrderPaymentConfirmed());
}

Order類的抽象基類定義了Update方法。下麵的代碼示例顯示了這個方法以及EventSourced類中的IdVersion屬性。

private readonly Guid id;
private int version = -1;

protected EventSourced(Guid id)
{
    this.id = id;
}

public int Version { get { return this.version; } }

protected void Update(VersionedEvent e)
{
    e.SourceId = this.Id;
    e.Version = this.version + 1;
    this.handlers[e.GetType()].Invoke(e);
    this.version = e.Version;
    this.pendingEvents.Add(e);
}

Update方法設置Id並遞增聚合的版本。它還確定應該調用聚合中的哪個事件處理程式來處理事件類型。

Markus(軟體開發人員)發言:

每次系統更新聚合的狀態時,都會增加聚合的版本號。

下麵的代碼示例顯示Order類中的事件處理程式方法,這些方法是在調用上面顯示的命令方法時調用的。

private void OnOrderPartiallyReserved(OrderPartiallyReserved e)
{
    this.seats = e.Seats.ToList();
}

private void OnOrderReservationCompleted(OrderReservationCompleted e)
{
    this.seats = e.Seats.ToList();
}

private void OnOrderExpired(OrderExpired e)
{
}

private void OnOrderPaymentConfirmed(OrderPaymentConfirmed e)
{
    this.isConfirmed = true;
}

這些方法更新聚合的狀態。

聚合必須能夠處理來自其他聚合的事件和它自己引發的事件。Order類中的受保護構造函數列出Order聚合可以處理的所有事件。

protected Order()
{
    base.Handles<OrderPlaced>(this.OnOrderPlaced);
    base.Handles<OrderUpdated>(this.OnOrderUpdated);
    base.Handles<OrderPartiallyReserved>(this.OnOrderPartiallyReserved);
    base.Handles<OrderReservationCompleted>(this.OnOrderReservationCompleted);
    base.Handles<OrderExpired>(this.OnOrderExpired);
    base.Handles<OrderPaymentConfirmed>(this.OnOrderPaymentConfirmed);
    base.Handles<OrderRegistrantAssigned>(this.OnOrderRegistrantAssigned);
}

事件持久化

當聚合在EventSourcedAggregateRoot類的Update方法中處理事件時,它將該事件添加到掛起事件的私有列表中。此列表將在名為Events的類(是EventSourced抽象類的實現類)中暴露成IEnumerable類型的公開屬性。

來自OrderCommandHandler類的以下代碼示例展示了處理程式如何調用Order類中的方法來處理命令,然後使用存儲庫將所有掛起事件附加到存儲中,從而持久存儲Order聚合的當前狀態。

public void Handle(MarkSeatsAsReserved command)
{
    var order = repository.Find(command.OrderId);

    if (order != null)
    {
        order.MarkAsReserved(command.Expiration, command.Seats);
        repository.Save(order);
    }
}

下麵的代碼示例顯示了SqlEventSourcedRepository類中Save方法的初始簡單實現。

備註:這些示例引用的是一個基於SQL Server實現的事件存儲。這是最初的方法,後來被基於Azure表存儲的實現所取代。基於SQL server實現的事件存儲仍然保留在解決方案中,這是為了方便您可以在本地運行應用程式,並使用這個實現來避免對Azure的任何依賴。
public void Save(T eventSourced)
{
    // TODO: guarantee that only incremental versions of the event are stored
    var events = eventSourced.Events.ToArray();
    using (var context = this.contextFactory.Invoke())
    {
        foreach (var e in events)
        {
            using (var stream = new MemoryStream())
            {
                this.serializer.Serialize(stream, e);
                var serialized = new Event { AggregateId = e.SourceId, Version = e.Version, Payload = stream.ToArray() };
                context.Set<Event>().Add(serialized);
            }
        }

        context.SaveChanges();
    }

    // TODO: guarantee delivery or roll back, or have a way to resume after a system crash
    this.eventBus.Publish(events);
}

通過重播事件來重建狀態

當處理程式類從存儲中載入聚合實例時,它通過重播存儲的事件流來載入實例的狀態。

Poe(IT運維人員)發言:

我們後來發現,使用事件源並能夠重播事件對於分析運行在雲中的生產系統中的bug是非常寶貴的技術。我們可以創建事件存儲的本地副本,然後在本地重播事件流,併在Visual Studio中調試應用程式,以準確理解生產系統中發生了什麼。

下麵來自OrderCommandHandler類的代碼示例顯示瞭如何調用存儲庫中的Find方法來啟動此過程。

public void Handle(MarkSeatsAsReserved command)
{
    var order = repository.Find(command.OrderId);

    ...
}

下麵的代碼示例顯示了SqlEventSourcedRepository類如何載入與聚合關聯的事件流。

Jana(軟體架構師)發言:

該團隊後來使用Azure表而不是SqlEventSourcedRepository開發了一個簡單的事件存儲。下一節將描述這種基於Azure表存儲的實現。

public T Find(Guid id)
{
    using (var context = this.contextFactory.Invoke())
    {
        var deserialized = context.Set<Event>()
            .Where(x => x.AggregateId == id)
            .OrderBy(x => x.Version)
            .AsEnumerable()
            .Select(x => this.serializer.Deserialize(new MemoryStream(x.Payload)))
            .Cast<IVersionedEvent>()
            .AsCachedAnyEnumerable();

        if (deserialized.Any())
        {
            return entityFactory.Invoke(id, deserialized);
        }

        return null;
    }
}

下麵的代碼示例顯示了當前面的代碼調用Invoke方法時候,Order類中的構造函數是怎樣從自己的事件流里重建狀態的。

public Order(Guid id, IEnumerable<IVersionedEvent> history) : this(id)
{
    this.LoadFrom(history);
}

LoadFrom方法在EventSourced類中定義,如下麵的代碼示例所示。對於歷史中存儲的每個事件,它確定要在Order類中調用的適當處理程式方法,並更新聚合實例的版本號。

protected void LoadFrom(IEnumerable<IVersionedEvent> pastEvents)
{
    foreach (var e in pastEvents)
    {
        this.handlers[e.GetType()].Invoke(e);
        this.version = e.Version;
    }
}

簡單事件存儲實現的一些問題

前面幾節中概述的事件源和事件存儲的簡單實現有許多缺點。下麵的列表列出了在生產質量的實現中應該剋服的一些缺點。

  • SqlEventRepository類中的Save方法不能保證將事件持久存儲併發布到消息傳遞基礎設施。失敗可能導致事件被保存到存儲區,但不會發佈。
  • 沒有檢查當系統持久保存一個事件時,它是否是比前一個事件晚一些的事件。事件需要被按順序存儲。
  • 對於事件流中有大量事件的聚合實例,沒有適當的優化。這可能會在重播事件時導致性能問題。

基於Azure表的事件存儲

基於Azure表實現的事件存儲解決了簡單的基於SQL server實現的事件存儲的一些缺點。然而,在這一點上,它仍然不是一個生產質量的實現。

團隊設計此實現是為了確保事件既被持久化到存儲中,又被髮布在消息匯流排上。為了實現這一點,它使用了Azure表的事務功能。

Markus(軟體開發人員)發言:

Azure表存儲支持跨共用相同分區鍵的記錄的事務。

EventStore類最初保存要持久化的每個事件的兩個副本。一個副本是該事件的永久記錄,另一個副本成為必須在Azure服務匯流排上發佈的事件虛擬隊列的一部分。下麵的代碼示例顯示了EventStore類中的Save方法。首碼“Unpublished”標識事件的副本,該副本是未發佈事件的虛擬隊列的一部分。

public void Save(string partitionKey, IEnumerable<EventData> events)
{
    var context = this.tableClient.GetDataServiceContext();
    foreach (var eventData in events)
    {
        var formattedVersion = eventData.Version.ToString("D10");
        context.AddObject(
            this.tableName,
            new EventTableServiceEntity
                {
                    PartitionKey = partitionKey,
                    RowKey = formattedVersion,
                    SourceId = eventData.SourceId,
                    SourceType = eventData.SourceType,
                    EventType = eventData.EventType,
                    Payload = eventData.Payload
                });

        // Add a duplicate of this event to the Unpublished "queue"
        context.AddObject(
            this.tableName,
            new EventTableServiceEntity
            {
                PartitionKey = partitionKey,
                RowKey = UnpublishedRowKeyPrefix + formattedVersion,
                SourceId = eventData.SourceId,
                SourceType = eventData.SourceType,
                EventType = eventData.EventType,
                Payload = eventData.Payload
            });

    }

    try
    {
        this.eventStoreRetryPolicy.ExecuteAction(() => context.SaveChanges(SaveChangesOptions.Batch));
    }
    catch (DataServiceRequestException ex)
    {
        var inner = ex.InnerException as DataServiceClientException;
        if (inner != null && inner.StatusCode == (int)HttpStatusCode.Conflict)
        {
            throw new ConcurrencyException();
        }

        throw;
    }
}

備註:此代碼示例還說明瞭如何使用重覆鍵錯誤來標識併發錯誤。

repository類中的Save方法如下所示。此方法由事件處理程式類調用,它調用前面代碼示例中所示的Save方法,並調用EventStoreBusPublisher類的SendAsync方法。

public void Save(T eventSourced)
{
    var events = eventSourced.Events.ToArray();
    var serialized = events.Select(this.Serialize);

    var partitionKey = this.GetPartitionKey(eventSourced.Id);
    this.eventStore.Save(partitionKey, serialized);

    this.publisher.SendAsync(partitionKey);
}

EventStoreBusPublisher類負責從Azure表存儲中的虛擬隊列中讀取聚合的未發佈事件,將事件發佈到Azure服務匯流排上,然後從虛擬隊列中刪除未發佈的事件。

如果系統在將事件發佈到Azure服務匯流排和從虛擬隊列中刪除事件之間失敗,那麼當應用程式重新啟動時,將第二次發佈事件。為了避免重覆發佈事件引起的問題,Azure服務匯流排被配置為檢測重覆消息並忽略它們。

Markus(軟體開發人員)發言:

在出現故障的情況下,系統必須包含一種機制,用於掃描表存儲中的所有分區,尋找包含未發佈事件的聚合,然後發佈這些事件。這個過程需要一些時間來運行,但是只需要在應用程式重新啟動時運行。

計算總數

為了保證其自主性,訂單和註冊限界上下文在不訪問會議管理限界上下文的情況下計算訂單總數。會議管理限界上下文負責維護會議座位的價格。

每當業務客戶添加新的座位類型或更改座位的價格時,會議管理限界上下文就會引發一個事件。訂單和註冊限界上下文將處理這些事件,並將信息作為其讀模型的一部分保存(詳細信息,請參考解決方案中的ConferenceViewModelGenerator類)。

當訂單聚合計算訂單總數時,它使用讀模型提供的數據。詳細信息請參考訂單聚合和PricingService類中的MarkAsReserved方法。

Jana(軟體架構師)發言:

當註冊者向訂單添加座位時,UI還動態顯示計算的總數。應用程式使用JavaScript計算這個值。當註冊者付款時,系統使用訂單總數計算的總數。

對測試的影響

Markus(軟體開發人員)發言:

不要讓通過的單元測試使您產生錯誤的安全感。當您實現CQRS模式時,有很多靈活的部分。您需要測試它們是否都能正確地協同工作。

Markus(軟體開發人員)發言:

不要忘記為讀模型創建單元測試。讀模型生成器上的單元測試在V1版本發佈之前就發現過一個bug,系統在更新訂單時刪除了訂單項。

時間問題

當業務客戶創建新的座位類型時,其中有一個驗收測試來驗證系統的行為。測試中的關鍵步驟是創建一個會議,為會議創建一個新的座位類型,然後發佈會議。這將引發相應的事件序列:ConferenceCreatedSeatCreatedConferencePublished

訂單和註冊限界上下文處理這些集成事件。測試確定訂單和註冊限界上下文接收這些事件的順序與會議管理限界上下文發送這些事件的順序不同。

Azure服務匯流排只提供先入先出(FIFO),因此,它可能不會按照事件發送的順序交付事件。在這個場景中,也有可能出現問題,因為在測試中創建消息並將其交付給Azure服務匯流排的步驟所花費的時間不同。在測試步驟之間引入人為的延遲為這個問題提供了一個臨時的解決方案。

在V2版本中,團隊計劃解決消息排序的一般問題,或者修改基礎設施以確保正確的排序,或者在消息確實出現順序錯誤時使系統更加健壯。

關於領域專家

在第4章“擴展和增強訂單和註冊限界上下文”中,您看到了領域專家如何參與設計驗收測試,以及他的參與如何幫助澄清領域知識。

您還應該確保領域專家參加錯誤分類會議。他或她可以幫助闡明系統的預期行為,並且在討論期間可以發現新的用戶場景。例如,對與在會議管理限界上下文中取消發佈會議相關的bug進行分類時,領

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 轉自:http://www.inbeijing.org/archives/1139 css控制Button 按鈕的點擊時候出現藍色邊框的問題 添加css屬性,這樣在點擊安按鈕的時候就不會有藍色邊框了。 button{ outline:none; } ...
  • 您將會從這篇文章里學習到如何在當下的 Vue.js 開發領域取得一定的成績。 ...
  • [2019.06.20 學習筆記1] 1.父子元素 2.兄弟元素 ...
  • HTTP協議 1 HTTP請求狀態碼 當用戶試圖通過 HTTP 訪問一臺正在運行 Internet 信息服務 (IIS) 的伺服器上的內容時,IIS 返回一個表示該請求的狀態的數字代碼。狀態代碼可以指明具體請求是否已成功,還可以揭示請求失敗的確切原因。 1.1 1xx - 信息提示 這些狀態代碼表示 ...
  • 一、瀏覽器介紹 瀏覽器是網頁運行的平臺,常用的瀏覽器有 IE、火狐(Firefox)、谷歌(Chrome)、Safari和Opera等。我們平時稱為五大瀏覽器。 可以通過這個網址 http://tongji.baidu.com/data/browser 查看瀏覽器的占有的市場份額 二、瀏覽器內核 1 ...
  • [2019.06.19 學習筆記4] 1.標簽格式 <開始標簽></結束標簽> 不區分大小寫,而且規則寬鬆。推薦小寫,並且要求成對編寫。 2.閉合標簽 <h1>內容</h1> 3.自閉合標簽(沒有內容的標簽) <img/> ...
  • 技術人員在構思一項功能時,會同時在大腦中編織其實現細節! 不利的一面 這種編織過程就像本能難以避免,它有優勢,對現實可行性更有判斷,但不利的影響也很明顯! 1、容易陷入細節,破壞思維的整體性。 2、構思的實現複雜度會直接影響決策。 這種構思本身受個人經驗所限。事實上實際複雜度有不確定性,再加上複雜度 ...
  • 今天,早早的起床,被外面火辣辣的太陽曬紅了臉。洗漱完發現已經遲到了,又不能吃早餐,來到了教室,面對著操蛋的課程,很令人蛋疼。啊,,,,美好的一上午就這麼結束了。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...