進一步探索訂單和註冊的有界上下文。
「我明白,若是一我的想看些新鮮的東西,旅行並非沒有意義的。」儒勒·凡爾納,環遊世界80天
複製代碼
前一章詳細描述了訂單和註冊限界上下文。本章描述了在CQRS之旅的第二階段,團隊在這個限界上下文中所作的一些更改。html
本章的主題包括:前端
本章描述的Contoso會議管理系統並非該系統的最終版本。本旅程描述的是一個過程,所以一些設計決策和實現細節將在過程的後續步驟中更改。這些變化將在後面的章節中描述。ios
本章使用了一些術語,咱們將在下一章進行描述。有關更多細節和可能的替代定義,請參閱參考指南中的「深刻CQRS和ES」。git
Command(命令):命令是要求系統執行更改系統狀態的操做。命令是必須服從(執行)的一種指令,例如:MakeSeatReservation。在這個限界上下文中,命令要麼來自用戶發起請求時的UI,要麼來自流程管理器(當流程管理器指示聚合執行某個操做時)。單個接收方處理一個命令。命令總線(command bus)傳輸命令,而後命令處理程序將這些命令發送到聚合。發送命令是一個沒有返回值的異步操做。github
Event(事件):事件就是系統中發生的一些事情,一般是一個命令的結果。領域模型中的聚合會引起(raise)事件。多個事件訂閱者(subscribers)能夠處理特定的事件。聚合將事件發佈到事件總線, 處理程序訂閱特定類型的事件,事件總線(event bus)將事件傳遞給訂閱者。在這個限界上下文中,惟一的訂閱者是流程管理器。web
流程管理器。在這個限界上下文中,流程管理器是一個協調領域域中聚合行爲的類。流程管理器訂閱聚合引起的事件,而後遵循一組簡單的規則來肯定發送一個或一組命令。流程管理器不包含任何業務邏輯,它惟一的邏輯是肯定下一個發送的命令。流程管理器被實現爲一個狀態機,所以當它響應一個事件時,除了發送一個新命令外,還能夠更改其內部狀態。
Gregor Hohpe和Bobby Woolf合著的《Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions》(Addison-Wesley Professional, 2003)書中312頁講述了流程管理器實現模式。咱們的流程管理器就是依照這個模式實現的。數據庫
除了描述訂單和註冊限界上下文的一些更改和加強以外,本章還討論了兩個用戶故事的實現。c#
當註冊者建立會議座位的訂單時,系統生成一個5個字符的訂單訪問代碼,並經過電子郵件發送給註冊者。登記人可使用她的電子郵件地址和會議系統網站上的訂單訪問代碼做爲記錄定位器,以便稍後從系統中檢索訂單。註冊者可能但願檢索訂單以查看它,或者經過分配與會者到座位來完成註冊過程。瀏覽器
Carlos(領域專家)發言:
從商業的角度來看,對咱們來講,儘量地作到用戶友好是很重要的。咱們不想阻止或沒必要要地增長任何試圖註冊會議的人的負擔。所以,咱們不要求用戶在註冊以前在系統中建立賬戶,特別是要求用戶不管如何都必須在標準的結賬過程當中輸入大部分信息。緩存
當註冊者建立一個訂單時,系統將保留註冊者請求的座位,直到完成訂單或預訂過時。要完成訂單,註冊者必須提交她的詳細信息,如姓名和電子郵件地址,併成功付款。
爲了幫助註冊者,系統會顯示一個倒計時計時器,告訴她還有多少時間能夠在預約到期前完成訂單。
當註冊者建立一個訂單,她能夠申請不一樣數量的座位,而且這些座位類型能夠不相同。例如,登記人可要求五個會議座位和三個會前講習班座位。
該應用程序旨在部署到Microsoft Azure。在旅程的那個階段,應用程序由兩個角色組成,一個包含ASP.Net MVC Web應用程序的web角色和一個包含消息處理程序和領域對象的工做角色。應用程序在寫端和讀端都使用Azure SQL DataBase實例進行數據存儲。應用程序使用Azure服務總線來提供其消息傳遞基礎設施。下圖展現了這個高級體系結構。
在研究和測試解決方案時,能夠在本地運行它,可使用Azure compute emulator,也能夠直接運行MVC web應用程序,並運行承載消息處理程序和領域域對象的控制檯應用程序。在本地運行應用程序時,可使用本地SQL Server Express數據庫,並使用一個在SQL Server Express數據庫實現的簡單的消息傳遞基礎設施。
有關運行應用程序的選項的更多信息,請參見附錄1「發佈說明」。
本節介紹了在團隊旅程的當前階段,應用程序的一些關鍵地方,並介紹了團隊在處理這些地方時遇到的一些挑戰。
該系統使用訪問碼而不是密碼,這樣註冊者就不會被迫在該系統中設置賬戶。許多註冊者可能只使用系統一次,所以不須要建立一個帶有用戶ID和密碼的永久賬戶。
系統須要可以根據註冊者的電子郵件地址和訪問代碼快速檢索訂單信息。爲了提供最低程度的安全性,系統生成的訪問代碼不該該是可預測的,註冊者能夠檢索的訂單信息不該該包含任何敏感信息。
前一章重點介紹了寫端模型及其實現,在本章中,咱們將更詳細地探討讀端的實現。特別地,咱們將解釋如何從MVC控制器實現讀取模型和查詢機制。
在對CQRS模式的初步研究中,團隊決定使用數據庫中的SQL視圖做爲讀取端MVC控制器查詢數據的基礎數據源。爲了最小化讀端查詢必須執行的工做,這些SQL視圖提供了數據的反規範化(denormalised)版本。這些視圖目前與寫模型使用的規範化(normalized)表存在同一個數據庫中。
Jana(軟件架構師)發言:
該團隊將把數據庫分爲兩個部分,並在旅程的後期將探索其餘的選擇來從規範化的寫端推送數據到反規範化的讀端。有關使用Azure blob存儲而不是SQL表存儲讀取端數據的示例,請參見SeatAssignmentsViewModelGenerator類。
存儲讀端數據的一個常見選項是使用一組關係數據庫表來保存。您應該優化讀取端以實現快速讀取,所以存儲規範化數據一般沒有任何好處,由於這將須要複雜的查詢來爲客戶端構造數據。這意味着讀取端的目標應該是使查詢儘量簡單,並以可以快速有效地讀取的方式在數據庫中構建表。
Gary(CQRS專家)發言:
當人們選擇使用CQRS模式時,可伸縮的應用程序和響應式UI一般是明確的目標。優化讀端以提供對查詢的快速響應,同時保持資源利用率較低,這將幫助您實現這些目標。
Jana(軟件架構師)發言:
因爲錶鏈接操做過多,規範化數據庫模式可能沒法提供足夠快的響應時間。儘管關係數據庫技術有所進步,可是與單表讀取相比,JOIN操做仍然很是昂貴。
譯者注:讀取端/查詢端一般就是所說的前端UI,若是使用關係型數據庫的關係表來存儲UI層要展示的頁面數據。每次讀取都須要作鏈接查詢或屢次查詢。因此把讀取端須要的數據保存爲反規範的數據能夠實現快速讀取。這個反規範化(denormalised)能夠簡單理解爲,拋棄關係型數據庫的關係,存儲非關係型的數據。
一個須要重要考慮的地方就是讀取端用來查詢數據的接口。讀取端就如ASP.Net MVC程序Controller的Action裏發起的查詢請求。
在下圖中,讀取端(如MVC Controller裏的Action)調用ViewRepository類上的方法來請求它須要的數據。而後,ViewRepository類對數據庫中的非規範化數據運行查詢。
Jana(軟件架構師)發言:
倉儲(Repository)模式使用相似集合的接口在領域和數據映射層之間進行轉換,以訪問領域對象。有關更多信息,請參考Martin Fowler,Catalog of Patterns of Enterprise Application Architecture,Repository。
Contoso的團隊評估了實現ViewRepository類的兩種方法:使用IQueryable接口和使用非通用的數據訪問對象(DAOs)。
ViewRepository類考慮的一種方法是讓它返回一個IQueryable實例,該實例容許客戶端使用LINQ來指定其查詢。返回IQueryable實例很簡單,不少ORM框架均可以,例如Entity Framework或NHibernate,下面的代碼片斷演示了客戶端如何作此類查詢。
var ordersummary = repository.Query<OrderSummary>().Where(LINQ query to retrieve order summary);
var orderdetails = repository.Query<OrderDetails>().Where(LINQ query to retrieve order details);
複製代碼
這種方法有幾個優勢:
簡單
可測試性
Markus(軟件開發人員)發言:
在參考實現(RI)中,咱們使用Entity Framework,咱們根本不須要編寫任何代碼來獲取IQueryable實例。咱們也只有一個ViewRepository類。
可能有人反對這個方法,包括:
另外一種方法是讓ViewRepository暴露出一個Find方法和一個Get方法,以下面的代碼片斷所示。
var ordersummary = dao.FindAllSummarizedOrders(userId);
var orderdetails = dao.GetOrderDetails(orderId);
複製代碼
您還能夠選擇使用不一樣的DAO類。這將使訪問不一樣數據源變得更容易。
var ordersummary = OrderSummaryDAO.FindAll(userId);
var orderdetails = OrderDetailsDAO.Get(orderId);
複製代碼
這種方法有幾個優勢:
簡單
靈活性
性能
可測試性
可維護性
對這個方法可能的反對意見包括:
使用IQueryable接口能夠更容易地在UI中支持分頁、過濾和排序等功能。不管如何,若是開發人員意識到這一缺點並盡力交付基於任務的UI,那麼這應該不是問題。
UI層經過在讀取端查詢模型得到的訂單數據來顯示。UI顯示給註冊者的部分數據是關於部分已完成訂單的信息:訂單中的每種座位類型,請求的座位數量和可用的座位數量。這是系統僅在註冊者使用UI建立訂單時使用的臨時數據。企業只須要存儲關於實際購買座位的信息,而不須要存儲註冊者請求的座位和註冊者購買的座位之間的差別。
這樣作的結果是,關於註冊者請求多少座位的信息只須要存在於讀取端模型中。
Jana(軟件架構師)發言:
您不能將此信息存儲在HTTP Session中,由於註冊者可能在請求座位和完成訂單之間離開站點。
進一步的結果是,讀端的底層存儲不能是簡單的SQL視圖,由於它包含的數據沒有存儲在寫端的底層表存儲中。所以,必須使用事件將此信息傳遞給讀取方。
下面的架構圖顯示了訂單(Order)和可用座位(SeatsAvailability)聚合使用的全部命令和事件,以及訂單(Order)聚合如何經過引起事件將更改推送到讀取端。
OrderViewModelGenerator類處理OrderPlaced、OrderUpdated、OrderPartiallyReserved、OrderRegistrantAssigned和OrderReservationCompleted事件,並使用DraftOrder和DraftOrderItem實例將更改持久化到視圖表中。
Gary(CQRS專家)發言:
若是您提早閱讀第5章「準備發佈V1版本」,您將看到團隊擴展了事件的使用,並遷移了訂單和註冊上下文,以使用事件源。
在實現寫模型時,應該儘可能確保命令不多失敗。這將提供最佳的用戶體驗,並使您的應用程序更容易實現異步行爲。
團隊採用的一種方法是使用ASP.NET MVC中的模型驗證功能。
您應該當心區分系統錯誤和業務錯誤。系統錯誤的例子包括:
在許多狀況下,特別是在雲中,您能夠經過重試操做來處理這些錯誤。
Markus(軟件開發人員)發言:
來自Microsoft patterns & practices的Transient Fault Handling Application Block的設計目的是使任何Transient Fault更容易實現一致的重試行爲。它提供了一組針對Azure SQL數據庫、Azure存儲、Azure緩存和Azure服務總線的內置檢測策略,還容許您定義本身的策略。相似地,它提供了一組方便的內置重試策略,並支持自定義策略。更多信息請參見The Transient Fault Handling Application Block
業務錯誤應該有預先定好的邏輯響應。例如:
Gary(CQRS專家)發言:
您的領域專家應該幫助您識別可能發生的業務失敗,並肯定您處理它們的方法:使用自動化流程或手動方式。
向註冊者顯示完成訂單所需時間的倒計時器是系統中的業務的一部分,而不只僅是基礎設施的一部分。當註冊者建立一個訂單並預訂座位時,倒計時就開始了。即便登記人離開會議網站,倒計時仍在繼續。若是註冊用戶返回網站,UI必須可以顯示正確的倒計時值,所以,保留過時時間是讀模型中可用數據的一部分。
本節描述訂單和註冊限界上下文的實現的一些重要特性。您可能會發現擁有一份代碼副本頗有用,這樣您就能夠繼續學習了。您能夠從Download center下載一個副本,或者在GitHub上查看存儲庫中的代碼:github.com/mspnp/cqrs-…
不要指望代碼示例與參考實現中的代碼徹底匹配。本章描述了CQRS過程當中的一個步驟,可是隨着咱們瞭解更多並重構代碼,實現可能會發生變化。
複製代碼
註冊者可能須要檢索訂單,或者查看訂單,或者完成對參會人員座位的分配。這可能發生在不一樣的web會話中,所以註冊者必須提供一些信息來定位之前保存的訂單。
下面的代碼示例顯示Order類如何生成一個新的五個字符的訂單訪問代碼,該代碼做爲Order實例的一部分被持久化。
public string AccessCode { get; set; }
protected Order()
{
...
this.AccessCode = HandleGenerator.Generate(5);
}
複製代碼
要檢索訂單實例,註冊者必須提供其電子郵件地址和訂單訪問代碼。系統將使用這兩項來定位正確的Order。這是讀取端的邏輯。
下面的代碼示例來自web應用程序中的OrderController類,展現了MVC控制器如何使用LocateOrder方法向讀取端提交查詢,以發現惟一的OrderId值。這個Find action將OrderId值傳遞給一個Display action,該action將訂單信息顯示給註冊者。
[HttpPost]
public ActionResult Find(string email, string accessCode)
{
var orderId = orderDao.LocateOrder(email, accessCode);
if (!orderId.HasValue)
{
return RedirectToAction("Find", new { conferenceCode = this.ConferenceCode });
}
return RedirectToAction("Display", new { conferenceCode = this.ConferenceCode, orderId = orderId.Value });
}
複製代碼
當註冊者建立一個訂單並預訂座位時,這些座位將保留一段固定的時間。RegistrationProcessManager實例將預訂從可用座位(SeatsAvailability)聚合中轉發,它將預訂過時的時間傳遞給訂單(Order)聚合。下面的代碼示例顯示訂單(Order)聚合如何接收和存儲預訂過時時間。
public DateTime? ReservationExpirationDate { get; private set; }
public void MarkAsReserved(DateTime expirationDate, IEnumerable<SeatQuantity> seats)
{
...
this.ReservationExpirationDate = expirationDate;
this.Items.Clear();
this.Items.AddRange(seats.Select(seat => new OrderItem(seat.SeatType, seat.Quantity)));
}
複製代碼
Markus(軟件開發人員)發言:
在Order的構造函數中,ReservationExpirationDate最初被設置爲在Order實例化後的15分鐘。RegistrationProcessManager類可能會根據實際預訂的時間進行修改。實際時間指的是流程管理器向訂單(Order)聚合發送MarkSeatsAsReserved命令的時間。
當RegistrationProcessManager將MarkSeatsAsReserved命令發送到訂單(Order)聚合(攜帶UI將顯示的過時時間)時,它還向本身發送一條命令,以啓動釋放預訂座位的過程。這個ExpireRegistrationProcess命令在過時區間加上一個5分鐘的緩衝來保存。這個緩衝是爲了確保服務器之間的時間差不會致使RegistrationProcessManager類在UI中的倒計時器清零以前就釋放預留的座位。下面的代碼示例展現RegistrationProcessManager類,UI使用MarkSeatsAsReserved命令中的Expiration屬性來顯示倒計時器,而ExpireRegistrationProcess命令中的Delay屬性肯定什麼時候釋放保留的座位。
public void Handle(SeatsReserved message)
{
if (this.State == ProcessState.AwaitingReservationConfirmation)
{
var expirationTime = this.ReservationAutoExpiration.Value;
this.State = ProcessState.ReservationConfirmationReceived;
if (this.ExpirationCommandId == Guid.Empty)
{
var bufferTime = TimeSpan.FromMinutes(5);
var expirationCommand = new ExpireRegistrationProcess { ProcessId = this.Id };
this.ExpirationCommandId = expirationCommand.Id;
this.AddCommand(new Envelope<ICommand>(expirationCommand)
{
Delay = expirationTime.Subtract(DateTime.UtcNow).Add(bufferTime),
});
}
this.AddCommand(new MarkSeatsAsReserved
{
OrderId = this.OrderId,
Seats = message.ReservationDetails.ToList(),
Expiration = expirationTime,
});
}
...
}
複製代碼
MVC項目中的RegistrationController類在讀取端檢索訂單信息。DraftOrder類包含控制器使用ViewBag類傳遞給視圖的預定過時時間,以下面的代碼示例所示。
[HttpGet]
public ActionResult SpecifyRegistrantDetails(string conferenceCode, Guid orderId)
{
var repo = this.repositoryFactory();
using (repo as IDisposable)
{
var draftOrder = repo.Find<DraftOrder>(orderId);
var conference = repo.Query<Conference>()
.Where(c => c.Code == conferenceCode)
.FirstOrDefault();
this.ViewBag.ConferenceName = conference.Name;
this.ViewBag.ConferenceCode = conference.Code;
this.ViewBag.ExpirationDateUTCMilliseconds =
draftOrder.BookingExpirationDate.HasValue ?
((draftOrder.BookingExpirationDate.Value.Ticks - EpochTicks) / 10000L) : 0L;
this.ViewBag.OrderId = orderId;
return View(new AssignRegistrantDetails { OrderId = orderId });
}
}
複製代碼
而後MVC的視圖使用JavaScript顯示動畫倒計時器。
您應該確保應用程序中的MVC控制器發送給寫模型的任何命令都將成功。在將命令發送到寫模型以前,可使用MVC中的特性在客戶端和服務器端驗證命令。
Markus(軟件開發人員)發言:
客戶端驗證對用戶來講主要是比較方便,由於它不用往返於服務器就能夠幫助用戶正確完成表單填寫。但您仍然須要實現服務器端驗證,以確保在將數據轉發到寫模型以前對其進行過驗證。
下面的代碼示例顯示了AssignRegistrantDetails命令類,它使用DataAnnotations指定驗證需求;在本例中,要求FirstName、LastName和Email字段不爲空。
using System;
using System.ComponentModel.DataAnnotations;
using Common;
public class AssignRegistrantDetails : ICommand
{
public AssignRegistrantDetails()
{
this.Id = Guid.NewGuid();
}
public Guid Id { get; private set; }
public Guid OrderId { get; set; }
[Required(AllowEmptyStrings = false)]
public string FirstName { get; set; }
[Required(AllowEmptyStrings = false)]
public string LastName { get; set; }
[Required(AllowEmptyStrings = false)]
public string Email { get; set; }
}
複製代碼
MVC視圖使用這個命令類做爲它的模型類。下面的代碼示例來自SpecifyRegistrantDetails.cshtml文件,它顯示瞭如何填充模型。
@model Registration.Commands.AssignRegistrantDetails
...
<div class="editor-label">@Html.LabelFor(model => model.FirstName)</div><div class="editor-field">@Html.EditorFor(model => model.FirstName)</div>
<div class="editor-label">@Html.LabelFor(model => model.LastName)</div><div class="editor-field">@Html.EditorFor(model => model.LastName)</div>
<div class="editor-label">@Html.LabelFor(model => model.Email)</div><div class="editor-field">@Html.EditorFor(model => model.Email)</div>
複製代碼
Web.config文件根據DataAnnotations屬性配置客戶端驗證,以下面的代碼片斷所示:
<appSettings>
...
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
複製代碼
服務器端驗證發生在發送命令以前的控制器中。下面來自RegistrationController類的代碼示例展現了控制器如何使用IsValid屬性來驗證命令。請記住,這個示例使用的是命令的一個實例做爲模型。
[HttpPost]
public ActionResult SpecifyRegistrantDetails(string conferenceCode, Guid orderId, AssignRegistrantDetails command)
{
if (!ModelState.IsValid)
{
return SpecifyRegistrantDetails(conferenceCode, orderId);
}
this.commandBus.Send(command);
return RedirectToAction("SpecifyPaymentDetails", new { conferenceCode = conferenceCode, orderId = orderId });
}
複製代碼
有關其餘示例,請參見RegistrationController類中的RegisterToConference命令和StartRegistration action方法。
更多信息,請參考MSDN上的Models and Validation in ASP.NET MVC 。
關於訂單的一些信息只須要存在於讀取端。特別是,關於部分已完成訂單的信息只在UI中使用,而不是寫端領域模型保存的業務信息的一部分。
這意味着系統不能使用SQL視圖做爲讀取端上的底層存儲機制,由於視圖不包含它們所基於的表中不存在的數據。
系統將非規範化的訂單數據存儲在SQL數據庫實例中的兩個表中:OrdersView和OrderItemsView表。OrderItemsView表包含RequestedSeats列,該列包含僅存在於讀取端上的數據。
OrdersView表
列 | 說明 |
---|---|
OrderId | Order的惟一ID |
ReservationExpirationDate | 預訂座位的過時時間 |
StateValue | 訂單的狀態,包括:Created, PartiallyReserved, ReservationCompleted, Rejected, Confirmed |
RegistrantEmail | 預訂時填寫的Email地址 |
AccessCode | 訂單的訪問碼 |
OrderItemsView
列 | 說明 |
---|---|
OrderItemId | 訂單項的惟一ID |
SeatType | 預訂的座位類型 |
RequestedSeats | 請求預訂座位的數量 |
ReservedSeats | 預留座位的數量 |
OrderId | 關聯的父Order的ID |
要將這些表填充到讀模型中,讀端須要處理由寫端引起的事件,用它們對這些表進行寫操做。有關詳細信息,請參見上面章節中的架構圖。
OrderViewModelGenerator類處理這些事件並更新讀端存儲庫。
public class OrderViewModelGenerator :
IEventHandler<OrderPlaced>, IEventHandler<OrderUpdated>,
IEventHandler<OrderPartiallyReserved>, IEventHandler<OrderReservationCompleted>,
IEventHandler<OrderRegistrantAssigned>
{
private readonly Func<ConferenceRegistrationDbContext> contextFactory;
public OrderViewModelGenerator(Func<ConferenceRegistrationDbContext> contextFactory)
{
this.contextFactory = contextFactory;
}
public void Handle(OrderPlaced @event)
{
using (var context = this.contextFactory.Invoke())
{
var dto = new DraftOrder(@event.SourceId, DraftOrder.States.Created)
{
AccessCode = @event.AccessCode,
};
dto.Lines.AddRange(@event.Seats.Select(seat => new DraftOrderItem(seat.SeatType, seat.Quantity)));
context.Save(dto);
}
}
public void Handle(OrderRegistrantAssigned @event)
{
...
}
public void Handle(OrderUpdated @event)
{
...
}
public void Handle(OrderPartiallyReserved @event)
{
...
}
public void Handle(OrderReservationCompleted @event)
{
...
}
...
}
複製代碼
下面的代碼示例展現ConferenceRegistrationDbContext類:
public class ConferenceRegistrationDbContext : DbContext
{
...
public T Find<T>(Guid id) where T : class
{
return this.Set<T>().Find(id);
}
public IQueryable<T> Query<T>() where T : class
{
return this.Set<T>();
}
public void Save<T>(T entity) where T : class
{
var entry = this.Entry(entity);
if (entry.State == System.Data.EntityState.Detached)
this.Set<T>().Add(entity);
this.SaveChanges();
}
}
複製代碼
Jana(軟件架構師)發言:
注意,讀端中的這個ConferenceRegistrationDbContext類包含一個Save方法,以保存從寫端發送的更改,並經過OrderViewModelGenerator類來調用。
下面的代碼示例顯示了一個非通用的DAO類,MVC控制器使用該類在讀端查詢會議信息。它封裝了前面展現的ConferenceRegistrationDbContext類。
public class ConferenceDao : IConferenceDao
{
private readonly Func<ConferenceRegistrationDbContext> contextFactory;
public ConferenceDao(Func<ConferenceRegistrationDbContext> contextFactory)
{
this.contextFactory = contextFactory;
}
public ConferenceDetails GetConferenceDetails(string conferenceCode)
{
using (var context = this.contextFactory.Invoke())
{
return context
.Query<Conference>()
.Where(dto => dto.Code == conferenceCode)
.Select(x => new ConferenceDetails { Id = x.Id, Code = x.Code, Name = x.Name, Description = x.Description, StartDate = x.StartDate })
.FirstOrDefault();
}
}
public ConferenceAlias GetConferenceAlias(string conferenceCode)
{
...
}
public IList<SeatType> GetPublishedSeatTypes(Guid conferenceId)
{
...
}
}
複製代碼
Jana(軟件架構師)發言:
注意,這個ConferenceDao類只包含返回數據的方法。MVC控制器使用它來檢索要在UI中顯示的數據。
在咱們CQRS之旅的第一階段,領域包含一個ConferenceSeatsAvailabilty聚合根類,這是對會議剩餘座位數量進行的建模。在旅程的如今這個階段,團隊將ConferenceSeatsAvailabilty聚合替換爲SeatsAvailability,以反映特定會議可能有多種座位類型。例如,完整會議的席位、會前研討會的席位和雞尾酒會的席位。下圖顯示了新的SeatsAvailability聚合及其組成類。
這個聚合反應了下面兩個模型:
領域如今包括一個SeatQuantity值類型,您可使用它來表示特定座椅類型的數量。
以前,聚合會根據是否有足夠的座位數量來引起ReservationAccepted或ReservationRejected事件,如今,聚合引起一個SeatsReserved事件,該事件報告它能夠預訂多少個特定類型的座位。這意味着預留的座位數目可能與所要求的座位數目不相符。此信息被傳遞迴UI,以便註冊者決定如何繼續預訂。
您可能在最上面的架構圖中注意到,SeatsAvailability聚合包含一個AddSeats方法,但沒有相應的命令。AddSeats方法調整給定類型的可用座位總數。業務客戶負責進行任何此類調整,並在Conference Management限界上下文中進行。當可用座位總數發生更改時,Conference Management限界上下文將引起事件。而後,SeatsAvailability類在其處理程序中調用AddSeat方法來處理事件。
本節將討論在如今這個階段解決的一些測試問題。
在第3章「訂單和註冊限界上下文」中,您看到了一些UI原型,開發人員和領域專家一塊兒工做,以改進系統的一些功能需求。這些UI原型的計劃用途之一是爲系統造成一組驗收測試的基礎。
對於驗收測試方法,團隊有如下目標:
爲了實現這些目標,領域專家與測試團隊的成員配對,並使用SpecFlow來指定核心驗收測試。
使用SpecFlow定義驗收測試的第一步是使用SpecFlow notation。這些測試被保存爲feature文件在一個Visual Studio項目中。如下代碼示例來自於ConferenceConfiguration.feature文件,該文件在Features\UserInterface\Views\Management文件夾下。它顯示了Conference Management限界上下文的驗收測試。典型的SpecFlow測試場景由一組Given、When和Then語句組成。其中一些語句包含測試使用的數據。
Markus(軟件開發人員)發言:
事實上,SpecFlow feature文件使用Gherkin語言,這是一種專門爲行爲描述建立的領域特定語言(DSL)。
Feature: Conference configuration scenarios for creating and editing Conference settings
In order to create or update a Conference configuration
As a Business Customer
I want to be able to create or update a Conference and set its properties
Background:
Given the Business Customer selected the Create Conference option
Scenario: An existing unpublished Conference is selected and published
Given this conference information
| Owner | Email | Name | Description | Slug | Start | End |
| William Flash | william@fabrikam.com | CQRS2012P | CQRS summit 2012 conference (Published) | random | 05/02/2012 | 05/12/2012 |
And the Business Customer proceeds to create the Conference
When the Business Customer proceeds to publish the Conference
Then the state of the Conference changes to Published
Scenario: An existing Conference is edited and updated
Given an existing published conference with this information
| Owner | Email | Name | Description | Slug | Start | End |
| William Flash | william@fabrikam.com | CQRS2012U | CQRS summit 2012 conference (Original) | random | 05/02/2012 | 05/12/2012 |
And the Business Customer proceeds to edit the existing settings with this information
| Description |
| CQRS summit 2012 conference (Updated) |
When the Business Customer proceeds to save the changes
Then this information appears in the Conference settings
| Description |
| CQRS summit 2012 conference (Updated) |
...
複製代碼
Carlos(領域專家)發言:
我發現這些驗收測試是我向開發人員闡明系統預期行爲定義的好方法。
有關其餘示例,請參見源代碼裏的Conference.AcceptanceTests解決方案
feature文件中的驗收測試不能直接執行。您必須提供一些管道代碼來鏈接SpecFlow feature文件和應用程序。
有關實現的示例,請參見源代碼Conference.AcceptanceTests解決方案下的Conference.Specflow項目下的Steps文件夾中的類。
這些步驟使用兩種不一樣的方法實現
第一種運行測試的方法是模擬系統的一個用戶,它經過使用第三方開源庫WatiN直接驅動web瀏覽器來實現。這種方法的優勢是,它運行系統的方式和實際用戶與系統交互的的方式徹底相同,而且最初實現起來很簡單。然而,這些測試是脆弱的,將須要大量的維護工做來保持它們在UI和系統更改後也會更新成最新的。下面的代碼示例展現了這種方法的一個示例,定義了前面所示的feature文件中的一些Given、When和Then步驟。SpecFlow使用Given、When和Then標記把步驟和feature文件中的子句連接起來,並把它當作參數值傳遞給測試方法:
public class ConferenceConfigurationSteps : StepDefinition
{
...
[Given(@"the Business Customer proceeds to edit the existing settings with this information")]
public void GivenTheBusinessCustomerProceedToEditTheExistingSettignsWithThisInformation(Table table)
{
Browser.Click(Constants.UI.EditConferenceId);
PopulateConferenceInformation(table);
}
[Given(@"an existing published conference with this information")]
public void GivenAnExistingPublishedConferenceWithThisInformation(Table table)
{
ExistingConferenceWithThisInformation(table, true);
}
private void ExistingConferenceWithThisInformation(Table table, bool publish)
{
NavigateToCreateConferenceOption();
PopulateConferenceInformation(table, true);
CreateTheConference();
if(publish) PublishTheConference();
ScenarioContext.Current.Set(table.Rows[0]["Email"], Constants.EmailSessionKey);
ScenarioContext.Current.Set(Browser.FindText(Slug.FindBy), Constants.AccessCodeSessionKey);
}
...
[When(@"the Business Customer proceeds to save the changes")]
public void WhenTheBusinessCustomerProceedToSaveTheChanges()
{
Browser.Click(Constants.UI.UpdateConferenceId);
}
...
[Then(@"this information appears in the Conference settings")]
public void ThenThisInformationIsShowUpInTheConferenceSettings(Table table)
{
Assert.True(Browser.SafeContainsText(table.Rows[0][0]),
string.Format("The following text was not found on the page: {0}", table.Rows[0][0]));
}
private void PublishTheConference()
{
Browser.Click(Constants.UI.PublishConferenceId);
}
private void CreateTheConference()
{
ScenarioContext.Current.Browser().Click(Constants.UI.CreateConferenceId);
}
private void NavigateToCreateConferenceOption()
{
// Navigate to Registration page
Browser.GoTo(Constants.ConferenceManagementCreatePage);
}
private void PopulateConferenceInformation(Table table, bool create = false)
{
var row = table.Rows[0];
if (create)
{
Browser.SetInput("OwnerName", row["Owner"]);
Browser.SetInput("OwnerEmail", row["Email"]);
Browser.SetInput("name", row["Email"], "ConfirmEmail");
Browser.SetInput("Slug", Slug.CreateNew().Value);
}
Browser.SetInput("Tagline", Constants.UI.TagLine);
Browser.SetInput("Location", Constants.UI.Location);
Browser.SetInput("TwitterSearch", Constants.UI.TwitterSearch);
if (row.ContainsKey("Name")) Browser.SetInput("Name", row["Name"]);
if (row.ContainsKey("Description")) Browser.SetInput("Description", row["Description"]);
if (row.ContainsKey("Start")) Browser.SetInput("StartDate", row["Start"]);
if (row.ContainsKey("End")) Browser.SetInput("EndDate", row["End"]);
}
}
複製代碼
您能夠看到這種方法是如何模擬在Web瀏覽器中點擊UI元素並輸入文本的。
第二種測試方法是經過與MVC控制器類交互來實現。長遠的看,這種方法不會那麼脆弱,成本就是在最初須要一個更復雜的實現,這須要對系統的內部實現比較熟悉。下面的代碼示例展現了這種方法的一個示例。
首先,在Features\UserInterface\Controllers\Registration文件夾下的SelfRegistrationEndToEndWithControllers.feature文件展現了一個示例場景:
Scenario: End to end Registration implemented using controllers
Given the Registrant proceeds to make the Reservation
And these Order Items should be reserved
| seat type | quantity |
| General admission | 1 |
| Additional cocktail party | 1 |
And these Order Items should not be reserved
| seat type |
| CQRS Workshop |
And the Registrant enters these details
| first name | last name | email address |
| William | Flash | william@fabrikam.com |
And the Registrant proceeds to Checkout:Payment
When the Registrant proceeds to confirm the payment
Then the Order should be created with the following Order Items
| seat type | quantity |
| General admission | 1 |
| Additional cocktail party | 1 |
And the Registrant assigns these seats
| seat type | first name | last name | email address |
| General admission | William | Flash | William@fabrikam.com |
| Additional cocktail party | Jim | Corbin | Jim@litwareinc.com |
And these seats are assigned
| seat type | quantity |
| General admission | 1 |
| Additional cocktail party | 1 |
複製代碼
而後,展現了SelfRegistrationEndToEndWithControllersSteps類裏的一些測試步驟:
[Given(@"the Registrant proceeds to make the Reservation")]
public void GivenTheRegistrantProceedToMakeTheReservation()
{
var redirect = registrationController.StartRegistration(
registration, registrationController.ViewBag.OrderVersion) as RedirectToRouteResult;
Assert.NotNull(redirect);
// Perform external redirection
var timeout = DateTime.Now.Add(Constants.UI.WaitTimeout);
while (DateTime.Now < timeout && registrationViewModel == null)
{
//ReservationUnknown
var result = registrationController.SpecifyRegistrantAndPaymentDetails(
(Guid)redirect.RouteValues["orderId"], registrationController.ViewBag.OrderVersion);
Assert.IsNotType<RedirectToRouteResult>(result);
registrationViewModel = RegistrationHelper.GetModel<RegistrationViewModel>(result);
}
Assert.False(registrationViewModel == null, "Could not make the reservation and get the RegistrationViewModel");
}
...
[When(@"the Registrant proceeds to confirm the payment")]
public void WhenTheRegistrantProceedToConfirmThePayment()
{
using (var paymentController = RegistrationHelper.GetPaymentController())
{
paymentController.ThirdPartyProcessorPaymentAccepted(
conferenceInfo.Slug, (Guid) routeValues["paymentId"], " ");
}
}
...
[Then(@"the Order should be created with the following Order Items")]
public void ThenTheOrderShouldBeCreatedWithTheFollowingOrderItems(Table table)
{
draftOrder = RegistrationHelper.GetModel<DraftOrder>(registrationController.ThankYou(registrationViewModel.Order.OrderId));
Assert.NotNull(draftOrder);
foreach (var row in table.Rows)
{
var orderItem = draftOrder.Lines.FirstOrDefault(
l => l.SeatType == conferenceInfo.Seats.First(s => s.Description == row["seat type"]).Id);
Assert.NotNull(orderItem);
Assert.Equal(Int32.Parse(row["quantity"]), orderItem.ReservedSeats);
}
}
複製代碼
您能夠看到這種方法是如何直接使用RegistrationController類的。
在這些代碼示例中,您能夠看到是怎樣經過標記把SpecFlow feature文件和測試步驟代碼連接起來並傳遞參數的。
複製代碼
團隊選擇使用xUnit.net來實現測試步驟,要在Visual Studio裏運行這些測試,您可使用任何支持xUnit的第三方工具例如:ReSharper, CodeRush, TestDriven.NET等。
Jana(軟件架構師)發言:
請記住,這些驗收測試並非在系統上執行的惟一測試。主要的解決方案裏包括全面的單元測試和集成測試,測試團隊還對應用程序進行了探索性和性能測試。
關於使用CQRS模式和大量使用消息,有一個常見說法是這讓人很難理解系統是如何經過發送和接收消息把各個不一樣的部分配合在一塊兒的。這裏您能夠經過設計適當的單元測試來幫助別人理解您的基本代碼。
訂單聚合的第一個單元測試示例:
public class given_placed_order
{
...
private Order sut;
public given_placed_order()
{
this.sut = new Order(
OrderId, new[]
{
new OrderPlaced
{
ConferenceId = ConferenceId,
Seats = new[] { new SeatQuantity(SeatTypeId, 5) },
ReservationAutoExpiration = DateTime.UtcNow
}
});
}
[Fact]
public void when_updating_seats_then_updates_order_with_new_seats()
{
this.sut.UpdateSeats(new[] { new OrderItem(SeatTypeId, 20) });
var @event = (OrderUpdated)sut.Events.Single();
Assert.Equal(OrderId, @event.SourceId);
Assert.Equal(1, @event.Seats.Count());
Assert.Equal(20, @event.Seats.ElementAt(0).Quantity);
}
...
}
複製代碼
這個單元測試只是建立一個Order實例,並直接調用UpdateSeats方法。它不向閱讀測試代碼的人提供有關調用此方法中命令或事件的任何信息。
如今看第二個示例,它執行的是相同的測試,可是在本示例中,是經過發送命令來測試的:
public class given_placed_order
{
...
private EventSourcingTestHelper<Order> sut;
public given_placed_order()
{
this.sut = new EventSourcingTestHelper<Order>();
this.sut.Setup(new OrderCommandHandler(sut.Repository, pricingService.Object));
this.sut.Given(
new OrderPlaced
{
SourceId = OrderId,
ConferenceId = ConferenceId,
Seats = new[] { new SeatQuantity(SeatTypeId, 5) },
ReservationAutoExpiration = DateTime.UtcNow
});
}
[Fact]
public void when_updating_seats_then_updates_order_with_new_seats()
{
this.sut.When(new RegisterToConference { ConferenceId = ConferenceId, OrderId = OrderId, Seats = new[] { new SeatQuantity(SeatTypeId, 20) }});
var @event = sut.ThenHasSingle<OrderUpdated>();
Assert.Equal(OrderId, @event.SourceId);
Assert.Equal(1, @event.Seats.Count());
Assert.Equal(20, @event.Seats.ElementAt(0).Quantity);
}
...
}
複製代碼
這個例子使用了一個helper類,它使您可以向Order實例發送命令。如今,閱讀測試的人能夠明白,當您發送RegisterToConference命令時,您指望看到OrderUpdated事件。
喬什·埃爾斯特講述了一個關於痛苦、解脫和學習的故事
複製代碼
本節描述CQRS諮詢委員會成員喬什·埃爾斯在探索Contoso會議管理系統的源代碼時所經歷的過程。
測試是很重要的
我曾經相信,優秀架構的應用程序很容易理解,無論代碼庫有多麼龐大。每當我理解應用程序行爲功能時遇到問題,都是代碼的問題,而不是個人問題。
永遠不要讓你的自負掩蓋住常識。
事實上,一直到我職業生涯的某個階段,我都尚未接觸到一個大型的、架構優秀的代碼基本。若是不是它走過來打個人臉,我根本就不知道它是什麼樣子。值得慶幸的是,隨着我閱讀代碼的經驗愈來愈豐富,我學會了區分那些不一樣。
備註:在任何結構良好的項目中,測試都是開發人員理解項目的基礎。各類命名約定,編碼風格,設計方法和使用模式的主題都包含在測試套件中,爲集成到代碼庫提供了一個很好的起點。這也是很好的代碼專業性實踐,熟能生巧!
複製代碼
克隆會議代碼以後,個人第一個動做是瀏覽測試。在閱讀了會議系統Visual Studio解決方案中的集成和單元測試套件以後,我將注意力集中在Conference.AcceptanceTests Visual Studio解決方案上,其中包含SpecFlow驗收測試。項目團隊的其餘成員已經對那些.feature文件作了一些初步的工做,因爲我不熟悉業務規則的細節,因此對我來講效果很好。把這些feature和代碼綁定是一種很好的方式,既能夠爲項目作出貢獻,又可讓人理解系統如何工做。
領域測試
當時個人目標是獲得一個像這樣的feature文件:
Feature: Self Registrant scenarios for making a Reservation for a Conference site with all Order Items initially available
In order to reserve Seats for a conference
As an Attendee
I want to be able to select an Order Item from one or many of the available Order Items and make a Reservation
Background:
Given the list of the available Order Items for the CQRS Summit 2012 conference with the slug code SelfRegFull
| seat type | rate | quota |
| General admission | $199 | 100 |
| CQRS Workshop | $500 | 100 |
| Additional cocktail party | $50 | 100 |
And the selected Order Items
| seat type | quantity |
| General admission | 1 |
| CQRS Workshop | 1 |
| Additional cocktail party | 1 |
Scenario: All the Order Items are available and all get reserved
When the Registrant proceeds to make the Reservation
Then the Reservation is confirmed for all the selected Order Items
And these Order Items should be reserved
| seat type |
| General admission |
| CQRS Workshop |
| Additional cocktail party |
And the total should read $749
And the countdown started
複製代碼
並將其綁定到執行操做、建立指望或做出斷言的代碼:
[Given(@"the '(.*)' site conference")]
public void GivenAConferenceNamed(string conference)
{
...
}
複製代碼
全部這些都位於"UI之下",可是在基礎概念之上。測試緊密關注整個解決方案領域的行爲,這就是爲何我將這些類型的測試稱爲領域測試。其餘術語,如行爲驅動開發(BDD),能夠用來描述這種類型的測試。
Jana(軟件架構師)發言:
這些「UI之下」測試也被稱爲皮下測試(參見Meszaros, G。Melnik, G的Acceptance Test Engineering Guide)。
重寫一遍已經在網站上實現的應用程序邏輯彷佛有點多餘,可是有如下幾個緣由值得花時間:
備註:爲何這些類型的測試是一個好主意?還有更多的緣由沒有列出來,可是對於本例來講,這裏列出的是那些重要的緣由。
Contoso會議管理系統的體系結構是鬆耦合的,利用消息將命令和事件傳遞給相關方。命令經過命令總線路由到單個處理程序,而事件則經過事件總線路由到它們的1個或多個處理程序。就消費應用程序而言,總線不綁定任何特定的技術,容許以對用戶透明的方式在整個系統中建立和使用任意的實現。
當涉及到鬆耦合消息體系結構的行爲測試時,另外一個好處是BDD(或相似風格的)測試自己不涉及應用程序代碼的內部工做。它們只關心被測試程序的可觀察行爲。這意味着對於SpecFlow測試,咱們只須要將一些命令發佈到總線,並經過根據實際的流量/數據斷言預期的消息流量和有效負載來檢查外部結果。
備註:在適當的地方,可使用mock和stub來進行這些類型的測試。一個適當的例子是使用mock出來的ICommandBus對象而不是真正的AzureCommandBus類型。但mock一個完整的領域服務是不合適的例子。儘可能少的使用mock,只把它限制在基礎設施方面,這樣你的生活和測試壓力都會小不少。
我剛剛花費了不少來描述事情是多麼的棒和簡單,哪裏有痛苦呢?痛苦在於理解一個系統中發生了什麼。鬆耦合的體系結構也有很差的一面:控制反轉和依賴注入等技術從本質上阻礙了代碼的可讀性,由於若是不仔細檢查容器的初始化,就永遠沒法肯定在特定的點注入了什麼具體的類。在journey的代碼中,IProcess接口是一種表示長時間運行的業務流程(也稱爲Sagas或流程管理器)的類,這些類負責協調不一樣聚合之間的業務邏輯。爲了維護系統數據和狀態的完整性、冪等性和事務性,它發出的命令的實際發送是各個持久化倉儲來實現的。因爲控制反轉和依賴注入對消費者隱藏了這些類型的詳細信息,因此它和系統的一些其餘屬性會形成一點困難在回答一些表面上瑣碎的問題時,好比:
因爲應用程序的依賴關係很是鬆散,許多傳統的代碼分析工具和方法要麼變得不那麼有用,要麼徹底沒用。
讓咱們以RegistrationProcessManager做爲示例,列出一些涉及到回答這些問題的啓發式內容。
打開RegistrationProcessManager.cs文件,注意,與許多流程管理器同樣,它有一個ProcessState枚舉。咱們注意進程的開始狀態:NotStarted。接下來,咱們要找到作下面事情之一的代碼:
找到源代碼中出現上述任何一種狀況或同時出現上述兩種狀況的代碼位置。在本例中,它是RegistrationProcessManagerRouter類中的Handle方法。重要提示:這並不必定意味着該流程是一個命令處理程序!流程管理器負責從存儲中建立和檢索聚合根(AR),以便將消息路由到AR,所以儘管它們的方法在名稱和簽名上與ICommandHandler實現相似,但它們並不實現處理命令的邏輯。
請注意當狀態發生變化時接收到的消息類型是做爲方法參數被傳入的,所以咱們如今須要找出消息的來源。
查找OrderPlaced的引用,找到一個或多個頂部(外部)組件,這些組件經過ICommandBus接口上的Send方法發出該類型的消息。
雖然啓發式的內容確定比這裏所提到的要多,可是這裏的這些內容極可能足夠證實了。即便討論交互也是一個至關漫長、繁瑣的過程。這很容易形成誤解。您能夠經過這種方式理解各類命令/事件消息傳遞交互,可是這種方式不是頗有效。
備註:通常來講,一我的在任什麼時候候都只能在腦子裏保持四到八個不一樣的想法。爲了說明這一律念,讓咱們保守地計算一下你須要在短時間記憶中同時保持的東西的數量,同時遵循上面的啓發:
進程類型+進程狀態屬性+初始狀態(NotStarted) + new()的位置+消息類型+中間路由類類型+ 2 *N^ N命令發出(位置、類型、步驟)+判別規則(邏輯也是數據!) > 8
複製代碼
當基礎設施需求混合到等式中時,信息飽和的問題會變得更加明顯。做爲咱們都是有能力的開發人員(對吧?),咱們能夠開始尋找方法來優化這些步驟,並提升相關信息的信噪比。
總之,咱們有兩個問題:
幸運的是,使用MIL(消息傳遞中間語言)能夠一箭雙鵰。
MIL一開始是一系列LINQPad腳本和代碼片斷,我建立這些腳本和代碼片斷是爲了在回答問題時幫助處理全部事情。最初,這些腳本完成的全部工做都是經過一個或多個項目程序集反映並輸出各類類型的消息和處理程序。在與團隊成員的討論中,很明顯其餘人也遇到了與我相同的問題。在與模式和實踐團隊成員進行了幾回聊天和頭腦風暴會議以後,咱們提出了引入一種小型領域特定語言(DSL)的想法,該語言將封裝所討論的交互。暫時命名爲SawMIL toolbox,它位於jelster.github.com/CqrsMessagi…,它提供了實用工具、腳本和示例,使您可以將MIL用做開發和分析流程管理器的一部分。
在MIL中,消息傳遞組件和交互以特定的方式表示:命令(由於它們是系統執行某些操做的請求)用?表示,好比DoSomething?。事件表示系統中發生的肯定的事情,所以得到一個!後綴,如SomethingHappened!
MIL的另外一個重要元素是消息發佈和接收。從消息源(如Azure服務總線、NServiceBus等)接收的消息老是在前面加上「->」符號,爲了讓示例暫時保持簡單,有一個可選的nil元素(句號.)。用於顯式地指示no-op(換句話說,沒有接收到任何消息)。下面的代碼片斷展現了nil元素語法的一個例子:
SendCustomerInvoice? -> .
CustomerInvoiceSent! -> .
複製代碼
一旦發佈了命令或事件,就須要對其進行處理。命令只有一個處理程序,而事件能夠有多個處理程序。MIL經過將處理程序的名稱放在消息傳遞操做的另外一側來表示消息與處理程序之間的這種關係,以下面的代碼片斷所示:
SendCustomerInvoice? -> CustomerInvoiceHandler
CustomerInvoiceSent! ->
-> CustomerNotificationHandler
-> AccountsAgeingViewModelGenerator
複製代碼
注意,命令和命令處理程序位於同一行,是由於命令和命令處理程序是1對1的。事件由於可能有多個事件處理程序,因此把他們放到多行上。
聚合根以@符號做爲前綴,使用過twitter的人都會很熟悉它。聚合根從不處理命令,但偶爾可能處理事件。聚合根是最多見的事件源,它引起事件以響應在聚合上調用的業務操做。可是,關於這些事件應該清楚的一點是,在大多數系統中,有其餘元素決定並實際執行領域事件的發佈。這是一個有趣的案例,其中業務和技術需求模糊了邊界,由基礎設施邏輯而不是應用程序或業務邏輯來知足需求。旅程代碼就是一個例子:爲了確保事件源和事件訂閱者之間的一致性,持久化聚合根的存儲庫的實現纔是負責將事件實際發佈到總線的。下面的代碼片斷顯示了AggregateRoot語法的一個示例:
SendCustomerInvoice? -> CustomerInvoiceHandler
@Invoice::CustomerInvoiceSent! -> .
複製代碼
在上面的示例中,一個名爲Scope上下文操做符的新語言元素出如今@AggregateRoot旁邊。範圍上下文元素由雙冒號(::)表示,它的兩個字符之間可能有空格,也可能沒有空格,用於標識兩個對象之間的關係。上面,聚合根 '@Invoice'生成CustomerSent!事件來響應CustomerInvoiceHandler調用的邏輯。下一個例子演示了在聚合根上使用Scope元素,它生成多個事件來響應單個命令:
SendCustomerInvoice? -> CustomerInvoiceHandler
@Invoice:
:CustomerInvoiceSent! -> .
:InvoiceAged! -> .
複製代碼
Scope上下文還用於表示不涉及基礎設施消息傳遞設備的元素內路由:
SendCustomerInvoice? -> CustomerInvoiceHandler
@Invoice::CustomerInvoiceSent! ->
-> InvoiceAgeingProcessRouter::InvoiceAgeingProcess
複製代碼
我將介紹的最後一個元素是State Change。狀態變化是跟蹤系統中發生的事情的最好方法之一,所以MIL將它們視爲一等公民。這些語句必須出如今它們本身的文本行中,並以「*」字符做爲前綴。這是MIL中惟一一次提到或出現任務,由於它很是重要!下面的代碼片斷顯示了State Change元素的一個例子:
SendCustomerInvoice? -> CustomerInvoiceHandler
@Invoice::CustomerInvoiceSent! ->
-> InvoiceAgegingProcessRouter::InvoiceAgeingProcess
*InvoiceAgeingProcess.ProcessState = Unpaid
複製代碼
咱們剛剛介紹了在鬆耦合應用程序中描述消息傳遞交互時使用的基本步驟。儘管所描述的交互只是可能交互的子集,可是MIL正在發展成爲一種簡潔地描述基於消息的系統交互的方法。不一樣的名詞和動詞(元素和動做)由不一樣的、有記憶意義的符號表示。這提供了一種跨基板(粘糊糊的人腦< - >硅CPU)的方法來通訊有關整個系統的有意義的信息。儘管該語言很好地描述了某些類型的消息傳遞交互,但它仍然是一項正在進行的工做,須要開發或改進該語言的許多元素和工具。這提供了一些很好的機會去爲OSS貢獻代碼,若是你一直在觀望或思考參與OSS去貢獻代碼,沒有時間猶豫了,如今就去jelster.github.com/CqrsMessagi…,fork倉庫,立刻開始吧!