日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

上下伸缩代码_CQRS之旅——旅程4(扩展和增强订单和注册限界上下文)

發(fā)布時(shí)間:2024/7/5 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 上下伸缩代码_CQRS之旅——旅程4(扩展和增强订单和注册限界上下文) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

旅程4:擴(kuò)展和增強(qiáng)訂單和注冊(cè)限界上下文

進(jìn)一步探索訂單和注冊(cè)的有界上下文。“我明白,如果一個(gè)人想看些新鮮的東西,旅行并不是沒有意義的。”儒勒·凡爾納,環(huán)游世界80天

對(duì)限界上下文的更改:

前一章詳細(xì)描述了訂單和注冊(cè)限界上下文。本章描述了在CQRS之旅的第二階段,團(tuán)隊(duì)在這個(gè)限界上下文中所做的一些更改。

本章的主題包括:

  • 改進(jìn)RegistrationProcessManager類中消息相關(guān)的工作方式。這說明了限界上下文中的聚合實(shí)例如何以復(fù)雜的方式進(jìn)行交互。
  • 實(shí)現(xiàn)一個(gè)記錄定位器,使注冊(cè)者能夠檢索她在前一個(gè)會(huì)話中保存的訂單。這說明了如何向?qū)懚?Write Side)添加一些額外的邏輯,使您能夠在不知道聚合實(shí)例惟一ID的情況下定位它。
  • 在UI中添加一個(gè)倒計(jì)時(shí)器,使注冊(cè)者能夠跟蹤他們需要在多長時(shí)間內(nèi)完成訂單。這說明了對(duì)寫端(Write Side)進(jìn)行的增強(qiáng),以支持在UI中顯示豐富的信息。
  • 同時(shí)支持多種座位類型的預(yù)定。例如,注冊(cè)者為會(huì)前的活動(dòng)申請(qǐng)5個(gè)座位,為會(huì)議申請(qǐng)8個(gè)座位。這需要在寫端(Write Side)使用更復(fù)雜的業(yè)務(wù)邏輯。
  • CQRS命令驗(yàn)證。這說明了如何在將CQRS命令發(fā)送到領(lǐng)域之前使用MVC中的模型驗(yàn)證特性來驗(yàn)證它們。

本章描述的Contoso會(huì)議管理系統(tǒng)并不是該系統(tǒng)的最終版本。本旅程描述的是一個(gè)過程,因此一些設(shè)計(jì)決策和實(shí)現(xiàn)細(xì)節(jié)將在過程的后續(xù)步驟中更改。這些變化將在后面的章節(jié)中描述。

本章的工作術(shù)語定義:

本章使用了一些術(shù)語,我們將在下面進(jìn)行描述。有關(guān)更多細(xì)節(jié)和可能的替代定義,請(qǐng)參閱參考指南中的“深入CQRS和ES”。

  • 命令(Command):命令是要求系統(tǒng)執(zhí)行更改系統(tǒng)狀態(tài)的操作。命令是必須服從(執(zhí)行)的一種指令,例如:MakeSeatReservation。在這個(gè)限界上下文中,命令要么來自用戶發(fā)起請(qǐng)求時(shí)的UI,要么來自流程管理器(當(dāng)流程管理器指示聚合執(zhí)行某個(gè)操作時(shí))。單個(gè)接收方處理一個(gè)命令。命令總線(command bus)傳輸命令,然后命令處理程序?qū)⑦@些命令發(fā)送到聚合。發(fā)送命令是一個(gè)沒有返回值的異步操作。
  • 事件(Event):事件就是系統(tǒng)中發(fā)生的一些事情,通常是一個(gè)命令的結(jié)果。領(lǐng)域模型中的聚合會(huì)引發(fā)(raise)事件。多個(gè)事件訂閱者(subscribers)可以處理特定的事件。聚合將事件發(fā)布到事件總線, 處理程序訂閱特定類型的事件,事件總線(event bus)將事件傳遞給訂閱者。在這個(gè)限界上下文中,唯一的訂閱者是流程管理器。
  • 流程管理器。在這個(gè)限界上下文中,流程管理器是一個(gè)協(xié)調(diào)領(lǐng)域域中聚合行為的類。流程管理器訂閱聚合引發(fā)的事件,然后遵循一組簡(jiǎn)單的規(guī)則來確定發(fā)送一個(gè)或一組命令。流程管理器不包含任何業(yè)務(wù)邏輯,它唯一的邏輯是確定下一個(gè)發(fā)送的命令。流程管理器被實(shí)現(xiàn)為一個(gè)狀態(tài)機(jī),因此當(dāng)它響應(yīng)一個(gè)事件時(shí),除了發(fā)送一個(gè)新命令外,還可以更改其內(nèi)部狀態(tài)。
    Gregor Hohpe和Bobby Woolf合著的《Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions》(Addison-Wesley Professional, 2003)書中312頁講述了流程管理器實(shí)現(xiàn)模式。我們的流程管理器就是依照這個(gè)模式實(shí)現(xiàn)的。

用戶故事(User stories)

除了描述訂單和注冊(cè)限界上下文的一些更改和增強(qiáng)之外,本章還討論了兩個(gè)用戶故事的實(shí)現(xiàn)。

使用記錄定位器作為登錄

當(dāng)注冊(cè)者創(chuàng)建會(huì)議座位的訂單時(shí),系統(tǒng)生成一個(gè)5個(gè)字符的訂單訪問代碼,并通過電子郵件發(fā)送給注冊(cè)者。登記人可以使用她的電子郵件地址和會(huì)議系統(tǒng)網(wǎng)站上的訂單訪問代碼作為記錄定位器,以便稍后從系統(tǒng)中檢索訂單。注冊(cè)者可能希望檢索訂單以查看它,或者通過分配與會(huì)者到座位來完成注冊(cè)過程。

Carlos(領(lǐng)域?qū)<?發(fā)言:
從商業(yè)的角度來看,對(duì)我們來說,盡可能地做到用戶友好是很重要的。我們不想阻止或不必要地增加任何試圖注冊(cè)會(huì)議的人的負(fù)擔(dān)。因此,我們不要求用戶在注冊(cè)之前在系統(tǒng)中創(chuàng)建帳戶,特別是要求用戶無論如何都必須在標(biāo)準(zhǔn)的結(jié)帳過程中輸入大部分信息。

告訴會(huì)議注冊(cè)者還剩余多少時(shí)間來完成訂單

當(dāng)注冊(cè)者創(chuàng)建一個(gè)訂單時(shí),系統(tǒng)將保留注冊(cè)者請(qǐng)求的座位,直到完成訂單或預(yù)訂過期。要完成訂單,注冊(cè)者必須提交她的詳細(xì)信息,如姓名和電子郵件地址,并成功付款。

為了幫助注冊(cè)者,系統(tǒng)會(huì)顯示一個(gè)倒計(jì)時(shí)計(jì)時(shí)器,告訴她還有多少時(shí)間可以在預(yù)定到期前完成訂單。

使注冊(cè)者能夠創(chuàng)建包含多個(gè)座位類型的訂單

當(dāng)注冊(cè)者創(chuàng)建一個(gè)訂單,她可以申請(qǐng)不同數(shù)量的座位,并且這些座位類型可以不相同。例如,登記人可要求五個(gè)會(huì)議座位和三個(gè)會(huì)前講習(xí)班座位。

架構(gòu)

該應(yīng)用程序旨在部署到Microsoft Azure。在旅程的這個(gè)階段,應(yīng)用程序由兩個(gè)角色組成,一個(gè)包含http://ASP.Net MVC Web應(yīng)用程序的web角色和一個(gè)包含消息處理程序和領(lǐng)域?qū)ο蟮墓ぷ鹘巧?yīng)用程序在寫端和讀端都使用Azure SQL DataBase實(shí)例進(jìn)行數(shù)據(jù)存儲(chǔ)。應(yīng)用程序使用Azure服務(wù)總線來提供其消息傳遞基礎(chǔ)設(shè)施。下圖展示了這個(gè)高級(jí)體系結(jié)構(gòu)。

在研究和測(cè)試解決方案時(shí),可以在本地運(yùn)行它,可以使用Azure compute emulator,也可以直接運(yùn)行MVC web應(yīng)用程序,并運(yùn)行承載消息處理程序和領(lǐng)域域?qū)ο蟮目刂婆_(tái)應(yīng)用程序。在本地運(yùn)行應(yīng)用程序時(shí),可以使用本地SQL Server Express數(shù)據(jù)庫,并使用一個(gè)在SQL Server Express數(shù)據(jù)庫實(shí)現(xiàn)的簡(jiǎn)單的消息傳遞基礎(chǔ)設(shè)施。

有關(guān)運(yùn)行應(yīng)用程序的選項(xiàng)的更多信息,請(qǐng)參見附錄1“發(fā)布說明”。

模式和概念

本節(jié)介紹了在團(tuán)隊(duì)旅程的當(dāng)前階段,應(yīng)用程序的一些關(guān)鍵地方,并介紹了團(tuán)隊(duì)在處理這些地方時(shí)遇到的一些挑戰(zhàn)。

記錄定位器

該系統(tǒng)使用訪問碼而不是密碼,這樣注冊(cè)者就不會(huì)被迫在該系統(tǒng)中設(shè)置帳戶。許多注冊(cè)者可能只使用系統(tǒng)一次,因此不需要?jiǎng)?chuàng)建一個(gè)帶有用戶ID和密碼的永久帳戶。

系統(tǒng)需要能夠根據(jù)注冊(cè)者的電子郵件地址和訪問代碼快速檢索訂單信息。為了提供最低程度的安全性,系統(tǒng)生成的訪問代碼不應(yīng)該是可預(yù)測(cè)的,注冊(cè)者可以檢索的訂單信息不應(yīng)該包含任何敏感信息。

在讀端查詢數(shù)據(jù)

前一章重點(diǎn)介紹了寫端模型及其實(shí)現(xiàn),在本章中,我們將更詳細(xì)地探討讀端的實(shí)現(xiàn)。特別地,我們將解釋如何從MVC控制器實(shí)現(xiàn)讀取模型和查詢機(jī)制。

在對(duì)CQRS模式的初步研究中,團(tuán)隊(duì)決定使用數(shù)據(jù)庫中的SQL視圖作為讀取端MVC控制器查詢數(shù)據(jù)的基礎(chǔ)數(shù)據(jù)源。為了最小化讀端查詢必須執(zhí)行的工作,這些SQL視圖提供了數(shù)據(jù)的反規(guī)范化(denormalised)版本。這些視圖目前與寫模型使用的規(guī)范化(normalized)表存在同一個(gè)數(shù)據(jù)庫中。

Jana(軟件架構(gòu)師)發(fā)言:
該團(tuán)隊(duì)將把數(shù)據(jù)庫分為兩個(gè)部分,并在旅程的后期將探索其他的選擇來從規(guī)范化的寫端推送數(shù)據(jù)到反規(guī)范化的讀端。有關(guān)使用Azure blob存儲(chǔ)而不是SQL表存儲(chǔ)讀取端數(shù)據(jù)的示例,請(qǐng)參見SeatAssignmentsViewModelGenerator類。

在數(shù)據(jù)庫存儲(chǔ)反規(guī)范化的視圖

存儲(chǔ)讀端數(shù)據(jù)的一個(gè)常見選項(xiàng)是使用一組關(guān)系數(shù)據(jù)庫表來保存。您應(yīng)該優(yōu)化讀取端以實(shí)現(xiàn)快速讀取,因此存儲(chǔ)規(guī)范化數(shù)據(jù)通常沒有任何好處,因?yàn)檫@將需要復(fù)雜的查詢來為客戶端構(gòu)造數(shù)據(jù)。這意味著讀取端的目標(biāo)應(yīng)該是使查詢盡可能簡(jiǎn)單,并以能夠快速有效地讀取的方式在數(shù)據(jù)庫中構(gòu)建表。

Gary(CQRS專家)發(fā)言:
當(dāng)人們選擇使用CQRS模式時(shí),可伸縮的應(yīng)用程序和響應(yīng)式UI通常是明確的目標(biāo)。優(yōu)化讀端以提供對(duì)查詢的快速響應(yīng),同時(shí)保持資源利用率較低,這將幫助您實(shí)現(xiàn)這些目標(biāo)。Jana(軟件架構(gòu)師)發(fā)言:
由于表連接操作過多,規(guī)范化數(shù)據(jù)庫模式可能無法提供足夠快的響應(yīng)時(shí)間。盡管關(guān)系數(shù)據(jù)庫技術(shù)有所進(jìn)步,但是與單表讀取相比,JOIN操作仍然非常昂貴。
譯者注:讀取端/查詢端通常就是所說的前端UI,如果使用關(guān)系型數(shù)據(jù)庫的關(guān)系表來存儲(chǔ)UI層要展現(xiàn)的頁面數(shù)據(jù)。每次讀取都需要做連接查詢或多次查詢。所以把讀取端需要的數(shù)據(jù)保存為反規(guī)范的數(shù)據(jù)可以實(shí)現(xiàn)快速讀取。這個(gè)反規(guī)范化(denormalised)可以簡(jiǎn)單理解為,拋棄關(guān)系型數(shù)據(jù)庫的關(guān)系,存儲(chǔ)非關(guān)系型的數(shù)據(jù)。

一個(gè)需要重要考慮的地方就是讀取端用來查詢數(shù)據(jù)的接口。讀取端就如http://ASP.Net MVC程序Controller的Action里發(fā)起的查詢請(qǐng)求。

在下圖中,讀取端(如MVC Controller里的Action)調(diào)用ViewRepository類上的方法來請(qǐng)求它需要的數(shù)據(jù)。然后,ViewRepository類對(duì)數(shù)據(jù)庫中的非規(guī)范化數(shù)據(jù)運(yùn)行查詢。

Jana(軟件架構(gòu)師)發(fā)言:
倉儲(chǔ)(Repository)模式使用類似集合的接口在領(lǐng)域和數(shù)據(jù)映射層之間進(jìn)行轉(zhuǎn)換,以訪問領(lǐng)域?qū)ο蟆S嘘P(guān)更多信息,請(qǐng)參考Martin Fowler,Catalog of Patterns of Enterprise Application Architecture,Repository。

Contoso的團(tuán)隊(duì)評(píng)估了實(shí)現(xiàn)ViewRepository類的兩種方法:使用IQueryable接口和使用非通用的數(shù)據(jù)訪問對(duì)象(DAOs)。

使用IQueryable接口

ViewRepository類考慮的一種方法是讓它返回一個(gè)IQueryable實(shí)例,該實(shí)例允許客戶端使用LINQ來指定其查詢。返回IQueryable實(shí)例很簡(jiǎn)單,很多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);

這種方法有幾個(gè)優(yōu)點(diǎn):

簡(jiǎn)單

  • 這種方法在底層數(shù)據(jù)庫上使用一個(gè)薄的抽象層。許多ORM都支持這種方法,它將您必須編寫的代碼量降到最低。
  • 您只需要定義一個(gè)倉儲(chǔ)和一個(gè)查詢方法。
  • 您不需要單獨(dú)的查詢對(duì)象。在讀端,查詢應(yīng)該很簡(jiǎn)單,因?yàn)槟呀?jīng)對(duì)寫端數(shù)據(jù)進(jìn)行了反規(guī)范化,以支持讀端。
  • 可以使用LINQ在客戶端上提供對(duì)過濾、分頁和排序等特性的支持。

可測(cè)試性

  • 您可以使用LINQ to object進(jìn)行Mocking。
Markus(軟件開發(fā)人員)發(fā)言:
在參考實(shí)現(xiàn)(RI)中,我們使用Entity Framework,我們根本不需要編寫任何代碼來獲取IQueryable實(shí)例。我們也只有一個(gè)ViewRepository類。

可能有人反對(duì)這個(gè)方法,包括:

  • 把數(shù)據(jù)存儲(chǔ)層替換為非關(guān)系型數(shù)據(jù)庫將很不容易,因?yàn)樾枰峁㊣Queryable實(shí)例。但無論如何,您總是可以為不同的限界上下文選擇使用適合的,不同的讀取端實(shí)現(xiàn)方式。
  • 客戶端在執(zhí)行操作的時(shí)候可能會(huì)濫用IQueryable接口,您應(yīng)該確保非規(guī)范化的數(shù)據(jù)完全滿足客戶的需求。
  • 使用IQueryable接口隱藏了查詢辦法。但是,由于在寫端對(duì)數(shù)據(jù)進(jìn)行過反規(guī)范化,因此對(duì)關(guān)系數(shù)據(jù)庫表的查詢沒辦法做更復(fù)雜的查詢。
  • 很難知道您的集成測(cè)試是否覆蓋了查詢方法的所有不同用途。

使用非通用DAOs

另一種方法是讓ViewRepository暴露出一個(gè)Find方法和一個(gè)Get方法,如下面的代碼片段所示。

var ordersummary = dao.FindAllSummarizedOrders(userId); var orderdetails = dao.GetOrderDetails(orderId);

您還可以選擇使用不同的DAO類。這將使訪問不同數(shù)據(jù)源變得更容易。

var ordersummary = OrderSummaryDAO.FindAll(userId); var orderdetails = OrderDetailsDAO.Get(orderId);

這種方法有幾個(gè)優(yōu)點(diǎn):

簡(jiǎn)單

  • 對(duì)客戶端來說,依賴關(guān)系更加清晰。例如,客戶端引用一個(gè)顯式的IOrderSummaryDAO實(shí)例,而不是一個(gè)通用的IViewRepository實(shí)例。 對(duì)于大多數(shù)查詢,只有一到兩種預(yù)定義的訪問對(duì)象的方法。不同的查詢通常返回不同的投射。

靈活性

  • Get和Find方法隱藏了數(shù)據(jù)存儲(chǔ)分區(qū)的細(xì)節(jié),還隱藏了使用ORM或顯式執(zhí)行SQL代碼等數(shù)據(jù)訪問方法。這使得將來更容易改變這些選擇。 Get和Find方法可以使用ORM、LINQ和IQueryable接口在背后從數(shù)據(jù)存儲(chǔ)中獲取數(shù)據(jù)。這是一個(gè)選擇,您可以建立在一個(gè)方法接一個(gè)方法的基礎(chǔ)上。

性能

  • 您可以輕松地優(yōu)化Find和Get方法運(yùn)行的查詢。數(shù)據(jù)訪問層執(zhí)行所有查詢。客戶端沒有任何風(fēng)險(xiǎn)試圖去做復(fù)雜的效率低的查詢。

可測(cè)試性

  • 為Find和Get方法創(chuàng)建單元測(cè)試要比為客戶端所有可能的LINQ查詢范圍創(chuàng)建合適的單元測(cè)試更容易。

可維護(hù)性

  • 所有查詢都定義在相同的位置DAO類中,從而更容易一致地修改系統(tǒng)。

對(duì)這個(gè)方法可能的反對(duì)意見包括:

使用IQueryable接口可以更容易地在UI中支持分頁、過濾和排序等功能。無論如何,如果開發(fā)人員意識(shí)到這一缺點(diǎn)并盡力交付基于任務(wù)的UI,那么這應(yīng)該不是問題。

把部分已完成的訂單信息提供給讀取端

UI層通過在讀取端查詢模型獲得的訂單數(shù)據(jù)來顯示。UI顯示給注冊(cè)者的部分?jǐn)?shù)據(jù)是關(guān)于部分已完成訂單的信息:訂單中的每種座位類型,請(qǐng)求的座位數(shù)量和可用的座位數(shù)量。這是系統(tǒng)僅在注冊(cè)者使用UI創(chuàng)建訂單時(shí)使用的臨時(shí)數(shù)據(jù)。企業(yè)只需要存儲(chǔ)關(guān)于實(shí)際購買座位的信息,而不需要存儲(chǔ)注冊(cè)者請(qǐng)求的座位和注冊(cè)者購買的座位之間的差異。

這樣做的結(jié)果是,關(guān)于注冊(cè)者請(qǐng)求多少座位的信息只需要存在于讀取端模型中。

Jana(軟件架構(gòu)師)發(fā)言:
您不能將此信息存儲(chǔ)在HTTP Session中,因?yàn)樽?cè)者可能在請(qǐng)求座位和完成訂單之間離開站點(diǎn)。

進(jìn)一步的結(jié)果是,讀端的底層存儲(chǔ)不能是簡(jiǎn)單的SQL視圖,因?yàn)樗臄?shù)據(jù)沒有存儲(chǔ)在寫端的底層表存儲(chǔ)中。因此,必須使用事件將此信息傳遞給讀取方。

下面的架構(gòu)圖顯示了訂單(Order)和可用座位(SeatsAvailability)聚合使用的所有命令和事件,以及訂單(Order)聚合如何通過引發(fā)事件將更改推送到讀取端。

OrderViewModelGenerator類處理OrderPlaced、OrderUpdated、OrderPartiallyReserved、OrderRegistrantAssigned和OrderReservationCompleted事件,并使用DraftOrder和DraftOrderItem實(shí)例將更改持久化到視圖表中。

Gary(CQRS專家)發(fā)言:
如果您提前閱讀第5章“準(zhǔn)備發(fā)布V1版本”,您將看到團(tuán)隊(duì)擴(kuò)展了事件的使用,并遷移了訂單和注冊(cè)上下文,以使用事件源。

CQRS命令校驗(yàn)

在實(shí)現(xiàn)寫模型時(shí),應(yīng)該盡量確保命令很少失敗。這將提供最佳的用戶體驗(yàn),并使您的應(yīng)用程序更容易實(shí)現(xiàn)異步行為。

團(tuán)隊(duì)采用的一種方法是使用http://ASP.NET MVC中的模型驗(yàn)證功能。

您應(yīng)該小心區(qū)分系統(tǒng)錯(cuò)誤和業(yè)務(wù)錯(cuò)誤。系統(tǒng)錯(cuò)誤的例子包括:

  • 由于消息傳遞基礎(chǔ)設(shè)施出現(xiàn)故障,無法傳遞消息。
  • 由于與數(shù)據(jù)庫的連接問題,數(shù)據(jù)沒有持久化。

在許多情況下,特別是在云中,您可以通過重試操作來處理這些錯(cuò)誤。

Markus(軟件開發(fā)人員)發(fā)言:
來自Microsoft patterns & practices的Transient Fault Handling Application Block的設(shè)計(jì)目的是使任何Transient Fault更容易實(shí)現(xiàn)一致的重試行為。它提供了一組針對(duì)Azure SQL數(shù)據(jù)庫、Azure存儲(chǔ)、Azure緩存和Azure服務(wù)總線的內(nèi)置檢測(cè)策略,還允許您定義自己的策略。類似地,它提供了一組方便的內(nèi)置重試策略,并支持自定義策略。更多信息請(qǐng)參見The Transient Fault Handling Application Block

業(yè)務(wù)錯(cuò)誤應(yīng)該有預(yù)先定好的邏輯響應(yīng)。例如:

  • 如果系統(tǒng)因?yàn)闆]有剩余的座位而無法預(yù)訂座位,那么它應(yīng)該將請(qǐng)求添加到等待列表中。
  • 如果信用卡支付失敗,用戶應(yīng)該有機(jī)會(huì)嘗試另一種信用卡,或者使用發(fā)票付款。
Gary(CQRS專家)發(fā)言:
您的領(lǐng)域?qū)<覒?yīng)該幫助您識(shí)別可能發(fā)生的業(yè)務(wù)失敗,并確定您處理它們的方法:使用自動(dòng)化流程或手動(dòng)方式。

倒計(jì)時(shí)器和讀取模型

向注冊(cè)者顯示完成訂單所需時(shí)間的倒計(jì)時(shí)器是系統(tǒng)中的業(yè)務(wù)的一部分,而不僅僅是基礎(chǔ)設(shè)施的一部分。當(dāng)注冊(cè)者創(chuàng)建一個(gè)訂單并預(yù)訂座位時(shí),倒計(jì)時(shí)就開始了。即使登記人離開會(huì)議網(wǎng)站,倒計(jì)時(shí)仍在繼續(xù)。如果注冊(cè)用戶返回網(wǎng)站,UI必須能夠顯示正確的倒計(jì)時(shí)值,因此,保留過期時(shí)間是讀模型中可用數(shù)據(jù)的一部分。

實(shí)現(xiàn)細(xì)節(jié)

本節(jié)描述訂單和注冊(cè)限界上下文的實(shí)現(xiàn)的一些重要特性。您可能會(huì)發(fā)現(xiàn)擁有一份代碼副本很有用,這樣您就可以繼續(xù)學(xué)習(xí)了。您可以從Download center下載一個(gè)副本,或者在GitHub上查看存儲(chǔ)庫中的代碼:https://github.com/mspnp/cqrs- jourcode

不要期望代碼示例與參考實(shí)現(xiàn)中的代碼完全匹配。本章描述了CQRS過程中的一個(gè)步驟,但是隨著我們了解更多并重構(gòu)代碼,實(shí)現(xiàn)可能會(huì)發(fā)生變化。

訂單訪問代碼和記錄定位器

注冊(cè)者可能需要檢索訂單,或者查看訂單,或者完成對(duì)參會(huì)人員座位的分配。這可能發(fā)生在不同的web會(huì)話中,因此注冊(cè)者必須提供一些信息來定位以前保存的訂單。

下面的代碼示例顯示Order類如何生成一個(gè)新的五個(gè)字符的訂單訪問代碼,該代碼作為Order實(shí)例的一部分被持久化。

public string AccessCode { get; set; }protected Order() {...this.AccessCode = HandleGenerator.Generate(5); }

要檢索訂單實(shí)例,注冊(cè)者必須提供其電子郵件地址和訂單訪問代碼。系統(tǒng)將使用這兩項(xiàng)來定位正確的Order。這是讀取端的邏輯。

下面的代碼示例來自web應(yīng)用程序中的OrderController類,展示了MVC控制器如何使用LocateOrder方法向讀取端提交查詢,以發(fā)現(xiàn)唯一的OrderId值。這個(gè)Find action將OrderId值傳遞給一個(gè)Display action,該action將訂單信息顯示給注冊(cè)者。

[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 }); }

倒計(jì)時(shí)器

當(dāng)注冊(cè)者創(chuàng)建一個(gè)訂單并預(yù)訂座位時(shí),這些座位將保留一段固定的時(shí)間。RegistrationProcessManager實(shí)例將預(yù)訂從可用座位(SeatsAvailability)聚合中轉(zhuǎn)發(fā),它將預(yù)訂過期的時(shí)間傳遞給訂單(Order)聚合。下面的代碼示例顯示訂單(Order)聚合如何接收和存儲(chǔ)預(yù)訂過期時(shí)間。

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(軟件開發(fā)人員)發(fā)言:
在Order的構(gòu)造函數(shù)中,ReservationExpirationDate最初被設(shè)置為在Order實(shí)例化后的15分鐘。RegistrationProcessManager類可能會(huì)根據(jù)實(shí)際預(yù)訂的時(shí)間進(jìn)行修改。實(shí)際時(shí)間指的是流程管理器向訂單(Order)聚合發(fā)送MarkSeatsAsReserved命令的時(shí)間。

當(dāng)RegistrationProcessManager將MarkSeatsAsReserved命令發(fā)送到訂單(Order)聚合(攜帶UI將顯示的過期時(shí)間)時(shí),它還向自己發(fā)送一條命令,以啟動(dòng)釋放預(yù)訂座位的過程。這個(gè)ExpireRegistrationProcess命令在過期區(qū)間加上一個(gè)5分鐘的緩沖來保存。這個(gè)緩沖是為了確保服務(wù)器之間的時(shí)間差不會(huì)導(dǎo)致RegistrationProcessManager類在UI中的倒計(jì)時(shí)器清零之前就釋放預(yù)留的座位。下面的代碼示例展示RegistrationProcessManager類,UI使用MarkSeatsAsReserved命令中的Expiration屬性來顯示倒計(jì)時(shí)器,而ExpireRegistrationProcess命令中的Delay屬性確定何時(shí)釋放保留的座位。

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項(xiàng)目中的RegistrationController類在讀取端檢索訂單信息。DraftOrder類包含控制器使用ViewBag類傳遞給視圖的預(yù)約過期時(shí)間,如下面的代碼示例所示。

[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顯示動(dòng)畫倒計(jì)時(shí)器。

使用http://ASP.NET MVC validation來驗(yàn)證命令

您應(yīng)該確保應(yīng)用程序中的MVC控制器發(fā)送給寫模型的任何命令都將成功。在將命令發(fā)送到寫模型之前,可以使用MVC中的特性在客戶端和服務(wù)器端驗(yàn)證命令。

Markus(軟件開發(fā)人員)發(fā)言:
客戶端驗(yàn)證對(duì)用戶來說主要是比較方便,因?yàn)樗挥猛涤诜?wù)器就可以幫助用戶正確完成表單填寫。但您仍然需要實(shí)現(xiàn)服務(wù)器端驗(yàn)證,以確保在將數(shù)據(jù)轉(zhuǎn)發(fā)到寫模型之前對(duì)其進(jìn)行過驗(yàn)證。

下面的代碼示例顯示了AssignRegistrantDetails命令類,它使用DataAnnotations指定驗(yàn)證需求;在本例中,要求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視圖使用這個(gè)命令類作為它的模型類。下面的代碼示例來自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文件根據(jù)DataAnnotations屬性配置客戶端驗(yàn)證,如下面的代碼片段所示:

<appSettings>...<add key="ClientValidationEnabled" value="true" /><add key="UnobtrusiveJavaScriptEnabled" value="true" /> </appSettings>

服務(wù)器端驗(yàn)證發(fā)生在發(fā)送命令之前的控制器中。下面來自RegistrationController類的代碼示例展示了控制器如何使用IsValid屬性來驗(yàn)證命令。請(qǐng)記住,這個(gè)示例使用的是命令的一個(gè)實(shí)例作為模型。

[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 }); }

有關(guān)其他示例,請(qǐng)參見RegistrationController類中的RegisterToConference命令和StartRegistration action方法。

更多信息,請(qǐng)參考MSDN上的Models and Validation in ASP.NET MVC 。

推送更新到讀端

關(guān)于訂單的一些信息只需要存在于讀取端。特別是,關(guān)于部分已完成訂單的信息只在UI中使用,而不是寫端領(lǐng)域模型保存的業(yè)務(wù)信息的一部分。

這意味著系統(tǒng)不能使用SQL視圖作為讀取端上的底層存儲(chǔ)機(jī)制,因?yàn)橐晥D不包含它們所基于的表中不存在的數(shù)據(jù)。

系統(tǒng)將非規(guī)范化的訂單數(shù)據(jù)存儲(chǔ)在SQL數(shù)據(jù)庫實(shí)例中的兩個(gè)表中:OrdersView和OrderItemsView表。OrderItemsView表包含RequestedSeats列,該列包含僅存在于讀取端上的數(shù)據(jù)。

OrdersView表

  • OrderId --> Order的唯一ID
  • ReservationExpirationDate --> 預(yù)訂座位的過期時(shí)間
  • StateValue --> 訂單的狀態(tài),包括:Created, PartiallyReserved, ReservationCompleted, Rejected, Confirmed
  • RegistrantEmail --> 預(yù)訂時(shí)填寫的Email地址
  • AccessCode --> 訂單的訪問碼

OrderItemsView

  • OrderItemId --> 訂單項(xiàng)的唯一ID
  • SeatType --> 預(yù)訂的座位類型
  • RequestedSeats --> 請(qǐng)求預(yù)訂座位的數(shù)量
  • ReservedSeats --> 預(yù)留座位的數(shù)量
  • OrderId --> 關(guān)聯(lián)的父Order的ID

要將這些表填充到讀模型中,讀端需要處理由寫端引發(fā)的事件,用它們對(duì)這些表進(jìn)行寫操作。有關(guān)詳細(xì)信息,請(qǐng)參見上面章節(jié)中的架構(gòu)圖。

OrderViewModelGenerator類處理這些事件并更新讀端存儲(chǔ)庫。

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(軟件架構(gòu)師)發(fā)言:
注意,讀端中的這個(gè)ConferenceRegistrationDbContext類包含一個(gè)Save方法,以保存從寫端發(fā)送的更改,并通過OrderViewModelGenerator類來調(diào)用。

在讀端查詢

下面的代碼示例顯示了一個(gè)非通用的DAO類,MVC控制器使用該類在讀端查詢會(huì)議信息。它封裝了前面展示的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(軟件架構(gòu)師)發(fā)言:
注意,這個(gè)ConferenceDao類只包含返回?cái)?shù)據(jù)的方法。MVC控制器使用它來檢索要在UI中顯示的數(shù)據(jù)。

重構(gòu)可用座位(SeatsAvailability)聚合

在我們CQRS之旅的第一階段,領(lǐng)域包含一個(gè)ConferenceSeatsAvailabilty聚合根類,這是對(duì)會(huì)議剩余座位數(shù)量進(jìn)行的建模。在旅程的現(xiàn)在這個(gè)階段,團(tuán)隊(duì)將ConferenceSeatsAvailabilty聚合替換為SeatsAvailability,以反映特定會(huì)議可能有多種座位類型。例如,完整會(huì)議的席位、會(huì)前研討會(huì)的席位和雞尾酒會(huì)的席位。下圖顯示了新的SeatsAvailability聚合及其組成類。

這個(gè)聚合反應(yīng)了下面兩個(gè)模型:

  • 一個(gè)會(huì)議可能有多種座位類型。
  • 每個(gè)座位類型可能有不同的座位數(shù)量。

領(lǐng)域現(xiàn)在包括一個(gè)SeatQuantity值類型,您可以使用它來表示特定座椅類型的數(shù)量。

之前,聚合會(huì)根據(jù)是否有足夠的座位數(shù)量來引發(fā)ReservationAccepted或ReservationRejected事件,現(xiàn)在,聚合引發(fā)一個(gè)SeatsReserved事件,該事件報(bào)告它可以預(yù)訂多少個(gè)特定類型的座位。這意味著預(yù)留的座位數(shù)目可能與所要求的座位數(shù)目不相符。此信息被傳遞回UI,以便注冊(cè)者決定如何繼續(xù)預(yù)訂。

AddSeats方法

您可能在最上面的架構(gòu)圖中注意到,SeatsAvailability聚合包含一個(gè)AddSeats方法,但沒有相應(yīng)的命令。AddSeats方法調(diào)整給定類型的可用座位總數(shù)。業(yè)務(wù)客戶負(fù)責(zé)進(jìn)行任何此類調(diào)整,并在Conference Management限界上下文中進(jìn)行。當(dāng)可用座位總數(shù)發(fā)生更改時(shí),Conference Management限界上下文將引發(fā)事件。然后,SeatsAvailability類在其處理程序中調(diào)用AddSeat方法來處理事件。

對(duì)測(cè)試的影響

本節(jié)將討論在現(xiàn)在這個(gè)階段解決的一些測(cè)試問題。

驗(yàn)收測(cè)試和領(lǐng)域?qū)<?/h3>

在第3章“訂單和注冊(cè)限界上下文”中,您看到了一些UI原型,開發(fā)人員和領(lǐng)域?qū)<乙黄鸸ぷ?#xff0c;以改進(jìn)系統(tǒng)的一些功能需求。這些UI原型的計(jì)劃用途之一是為系統(tǒng)形成一組驗(yàn)收測(cè)試的基礎(chǔ)。

對(duì)于驗(yàn)收測(cè)試方法,團(tuán)隊(duì)有以下目標(biāo):

  • 驗(yàn)收測(cè)試應(yīng)該以領(lǐng)域?qū)<夷軌蚶斫獾母袷角宄乇磉_(dá)出來。
  • 應(yīng)該可以自動(dòng)執(zhí)行驗(yàn)收測(cè)試。

為了實(shí)現(xiàn)這些目標(biāo),領(lǐng)域?qū)<遗c測(cè)試團(tuán)隊(duì)的成員配對(duì),并使用SpecFlow來指定核心驗(yàn)收測(cè)試。

使用SpecFlow feature來定義驗(yàn)收測(cè)試

使用SpecFlow定義驗(yàn)收測(cè)試的第一步是使用SpecFlow notation。這些測(cè)試被保存為feature文件在一個(gè)Visual Studio項(xiàng)目中。以下代碼示例來自于ConferenceConfiguration.feature文件,該文件在FeaturesUserInterfaceViewsManagement文件夾下。它顯示了Conference Management限界上下文的驗(yàn)收測(cè)試。典型的SpecFlow測(cè)試場(chǎng)景由一組Given、When和Then語句組成。其中一些語句包含測(cè)試使用的數(shù)據(jù)。

Markus(軟件開發(fā)人員)發(fā)言:
事實(shí)上,SpecFlow feature文件使用Gherkin語言,這是一種專門為行為描述創(chuàng)建的領(lǐng)域特定語言(DSL)。Feature: Conference configuration scenarios for creating and editing Conference settingsIn order to create or update a Conference configurationAs a Business CustomerI want to be able to create or update a Conference and set its propertiesBackground: Given the Business Customer selected the Create Conference optionScenario: 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 PublishedScenario: 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(領(lǐng)域?qū)<?發(fā)言:
我發(fā)現(xiàn)這些驗(yàn)收測(cè)試是我向開發(fā)人員闡明系統(tǒng)預(yù)期行為定義的好方法。

有關(guān)其他示例,請(qǐng)參見源代碼里的Conference.AcceptanceTests解決方案

讓測(cè)試可執(zhí)行

feature文件中的驗(yàn)收測(cè)試不能直接執(zhí)行。您必須提供一些管道代碼來連接SpecFlow feature文件和應(yīng)用程序。

有關(guān)實(shí)現(xiàn)的示例,請(qǐng)參見源代碼Conference.AcceptanceTests解決方案下的Conference.Specflow項(xiàng)目下的Steps文件夾中的類。

這些步驟使用兩種不同的方法實(shí)現(xiàn)

第一種運(yùn)行測(cè)試的方法是模擬系統(tǒng)的一個(gè)用戶,它通過使用第三方開源庫WatiN直接驅(qū)動(dòng)web瀏覽器來實(shí)現(xiàn)。這種方法的優(yōu)點(diǎn)是,它運(yùn)行系統(tǒng)的方式和實(shí)際用戶與系統(tǒng)交互的的方式完全相同,并且最初實(shí)現(xiàn)起來很簡(jiǎn)單。然而,這些測(cè)試是脆弱的,將需要大量的維護(hù)工作來保持它們?cè)赨I和系統(tǒng)更改后也會(huì)更新成最新的。下面的代碼示例展示了這種方法的一個(gè)示例,定義了前面所示的feature文件中的一些Given、When和Then步驟。SpecFlow使用Given、When和Then標(biāo)記把步驟和feature文件中的子句鏈接起來,并把它當(dāng)做參數(shù)值傳遞給測(cè)試方法:

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 pageBrowser.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瀏覽器中點(diǎn)擊UI元素并輸入文本的。

第二種測(cè)試方法是通過與MVC控制器類交互來實(shí)現(xiàn)。長遠(yuǎn)的看,這種方法不會(huì)那么脆弱,成本就是在最初需要一個(gè)更復(fù)雜的實(shí)現(xiàn),這需要對(duì)系統(tǒng)的內(nèi)部實(shí)現(xiàn)比較熟悉。下面的代碼示例展示了這種方法的一個(gè)示例。

首先,在FeaturesUserInterfaceControllersRegistration文件夾下的SelfRegistrationEndToEndWithControllers.feature文件展示了一個(gè)示例場(chǎng)景:

Scenario: End to end Registration implemented using controllersGiven the Registrant proceeds to make the ReservationAnd 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:PaymentWhen the Registrant proceeds to confirm the paymentThen 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類里的一些測(cè)試步驟:

[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 redirectionvar timeout = DateTime.Now.Add(Constants.UI.WaitTimeout);while (DateTime.Now < timeout && registrationViewModel == null){//ReservationUnknownvar 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類的。

在這些代碼示例中,您可以看到是怎樣通過標(biāo)記把SpecFlow feature文件和測(cè)試步驟代碼鏈接起來并傳遞參數(shù)的。

團(tuán)隊(duì)選擇使用xUnit.net來實(shí)現(xiàn)測(cè)試步驟,要在Visual Studio里運(yùn)行這些測(cè)試,您可以使用任何支持xUnit的第三方工具例如:ReSharper, CodeRush, TestDriven.NET等。

Jana(軟件架構(gòu)師)發(fā)言:
請(qǐng)記住,這些驗(yàn)收測(cè)試并不是在系統(tǒng)上執(zhí)行的唯一測(cè)試。主要的解決方案里包括全面的單元測(cè)試和集成測(cè)試,測(cè)試團(tuán)隊(duì)還對(duì)應(yīng)用程序進(jìn)行了探索性和性能測(cè)試。

使用測(cè)試來幫助開發(fā)人員理解消息流

關(guān)于使用CQRS模式和大量使用消息,有一個(gè)常見說法是這讓人很難理解系統(tǒng)是如何通過發(fā)送和接收消息把各個(gè)不同的部分配合在一起的。這里您可以通過設(shè)計(jì)適當(dāng)?shù)膯卧獪y(cè)試來幫助別人理解您的基本代碼。

訂單聚合的第一個(gè)單元測(cè)試示例:

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);}... }

這個(gè)單元測(cè)試只是創(chuàng)建一個(gè)Order實(shí)例,并直接調(diào)用UpdateSeats方法。它不向閱讀測(cè)試代碼的人提供有關(guān)調(diào)用此方法中命令或事件的任何信息。

現(xiàn)在看第二個(gè)示例,它執(zhí)行的是相同的測(cè)試,但是在本示例中,是通過發(fā)送命令來測(cè)試的:

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);}... }

這個(gè)例子使用了一個(gè)helper類,它使您能夠向Order實(shí)例發(fā)送命令。現(xiàn)在,閱讀測(cè)試的人可以明白,當(dāng)您發(fā)送RegisterToConference命令時(shí),您期望看到OrderUpdated事件。

代碼理解之旅

喬什·埃爾斯特講述了一個(gè)關(guān)于痛苦、解脫和學(xué)習(xí)的故事

本節(jié)描述CQRS咨詢委員會(huì)成員喬什·埃爾斯在探索Contoso會(huì)議管理系統(tǒng)的源代碼時(shí)所經(jīng)歷的過程。

測(cè)試是很重要的

我曾經(jīng)相信,優(yōu)秀架構(gòu)的應(yīng)用程序很容易理解,不管代碼庫有多么龐大。每當(dāng)我理解應(yīng)用程序行為功能時(shí)遇到問題,都是代碼的問題,而不是我的問題。

永遠(yuǎn)不要讓你的自負(fù)掩蓋住常識(shí)。

事實(shí)上,一直到我職業(yè)生涯的某個(gè)階段,我都還沒有接觸到一個(gè)大型的、架構(gòu)優(yōu)秀的代碼基本。如果不是它走過來打我的臉,我根本就不知道它是什么樣子。值得慶幸的是,隨著我閱讀代碼的經(jīng)驗(yàn)越來越豐富,我學(xué)會(huì)了區(qū)分那些不同。

備注:在任何結(jié)構(gòu)良好的項(xiàng)目中,測(cè)試都是開發(fā)人員理解項(xiàng)目的基礎(chǔ)。各種命名約定,編碼風(fēng)格,設(shè)計(jì)方法和使用模式的主題都包含在測(cè)試套件中,為集成到代碼庫提供了一個(gè)很好的起點(diǎn)。這也是很好的代碼專業(yè)性實(shí)踐,熟能生巧!

克隆會(huì)議代碼之后,我的第一個(gè)動(dòng)作是瀏覽測(cè)試。在閱讀了會(huì)議系統(tǒng)Visual Studio解決方案中的集成和單元測(cè)試套件之后,我將注意力集中在Conference.AcceptanceTests Visual Studio解決方案上,其中包含SpecFlow驗(yàn)收測(cè)試。項(xiàng)目團(tuán)隊(duì)的其他成員已經(jīng)對(duì)那些.feature文件做了一些初步的工作,由于我不熟悉業(yè)務(wù)規(guī)則的細(xì)節(jié),所以對(duì)我來說效果很好。把這些feature和代碼綁定是一種很好的方式,既可以為項(xiàng)目做出貢獻(xiàn),又可以讓人理解系統(tǒng)如何工作。

領(lǐng)域測(cè)試

當(dāng)時(shí)我的目標(biāo)是得到一個(gè)像這樣的feature文件:

Feature: Self Registrant scenarios for making a Reservation for a Conference site with all Order Items initially availableIn order to reserve Seats for a conferenceAs an AttendeeI want to be able to select an Order Item from one or many of the available Order Items and make a ReservationBackground: 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 reservedWhen the Registrant proceeds to make the Reservation Then the Reservation is confirmed for all the selected Order ItemsAnd these Order Items should be reserved| seat type || General admission || CQRS Workshop || Additional cocktail party |And the total should read $749And the countdown started

并將其綁定到執(zhí)行操作、創(chuàng)建期望或作出斷言的代碼:

[Given(@"the '(.*)' site conference")] public void GivenAConferenceNamed(string conference) {... }

所有這些都位于"UI之下",但是在基礎(chǔ)概念之上。測(cè)試緊密關(guān)注整個(gè)解決方案領(lǐng)域的行為,這就是為什么我將這些類型的測(cè)試稱為領(lǐng)域測(cè)試。其他術(shù)語,如行為驅(qū)動(dòng)開發(fā)(BDD),可以用來描述這種類型的測(cè)試。

Jana(軟件架構(gòu)師)發(fā)言:
這些“UI之下”測(cè)試也被稱為皮下測(cè)試(參見Meszaros, G。Melnik, G的Acceptance Test Engineering Guide)。

重寫一遍已經(jīng)在網(wǎng)站上實(shí)現(xiàn)的應(yīng)用程序邏輯似乎有點(diǎn)多余,但是有以下幾個(gè)原因值得花時(shí)間:

  • 您(由于某些原因)對(duì)網(wǎng)站或任何其他基礎(chǔ)設(shè)施部分的行為測(cè)試不感興趣。你只對(duì)領(lǐng)域有興趣,單元級(jí)和集成級(jí)的測(cè)試將驗(yàn)證代碼的功能是否正確,因此不需要重復(fù)這些測(cè)試。
  • 當(dāng)與產(chǎn)品所有者迭代用戶故事時(shí),將時(shí)間花在純粹的UI關(guān)注點(diǎn)上會(huì)拖慢反饋周期,降低反饋的質(zhì)量和有用性。
  • 考慮到不同的人在討論技術(shù)問題時(shí)使用的詞匯之間有時(shí)會(huì)出現(xiàn)很大的不匹配,用更抽象的術(shù)語討論一個(gè)功能可以更好的理解業(yè)務(wù)試圖解決的問題。
  • 在實(shí)現(xiàn)測(cè)試邏輯時(shí)遇到的障礙可以幫助提高系統(tǒng)的總體設(shè)計(jì)質(zhì)量。基礎(chǔ)設(shè)施代碼與應(yīng)用程序邏輯難以分離通常被視為一種壞味道。
備注:為什么這些類型的測(cè)試是一個(gè)好主意?還有更多的原因沒有列出來,但是對(duì)于本例來說,這里列出的是那些重要的原因。

Contoso會(huì)議管理系統(tǒng)的體系結(jié)構(gòu)是松耦合的,利用消息將命令和事件傳遞給相關(guān)方。命令通過命令總線路由到單個(gè)處理程序,而事件則通過事件總線路由到它們的1個(gè)或多個(gè)處理程序。就消費(fèi)應(yīng)用程序而言,總線不綁定任何特定的技術(shù),允許以對(duì)用戶透明的方式在整個(gè)系統(tǒng)中創(chuàng)建和使用任意的實(shí)現(xiàn)。

當(dāng)涉及到松耦合消息體系結(jié)構(gòu)的行為測(cè)試時(shí),另一個(gè)好處是BDD(或類似風(fēng)格的)測(cè)試本身不涉及應(yīng)用程序代碼的內(nèi)部工作。它們只關(guān)心被測(cè)試程序的可觀察行為。這意味著對(duì)于SpecFlow測(cè)試,我們只需要將一些命令發(fā)布到總線,并通過根據(jù)實(shí)際的流量/數(shù)據(jù)斷言預(yù)期的消息流量和有效負(fù)載來檢查外部結(jié)果。

備注:在適當(dāng)?shù)牡胤?#xff0c;可以使用mock和stub來進(jìn)行這些類型的測(cè)試。一個(gè)適當(dāng)?shù)睦邮鞘褂胢ock出來的ICommandBus對(duì)象而不是真正的AzureCommandBus類型。但mock一個(gè)完整的領(lǐng)域服務(wù)是不合適的例子。盡量少的使用mock,只把它限制在基礎(chǔ)設(shè)施方面,這樣你的生活和測(cè)試壓力都會(huì)小很多。

另一種情況

我剛剛花費(fèi)了很多來描述事情是多么的棒和簡(jiǎn)單,哪里有痛苦呢?痛苦在于理解一個(gè)系統(tǒng)中發(fā)生了什么。松耦合的體系結(jié)構(gòu)也有不好的一面:控制反轉(zhuǎn)和依賴注入等技術(shù)從本質(zhì)上阻礙了代碼的可讀性,因?yàn)槿绻蛔屑?xì)檢查容器的初始化,就永遠(yuǎn)無法確定在特定的點(diǎn)注入了什么具體的類。在journey的代碼中,IProcess接口是一種表示長時(shí)間運(yùn)行的業(yè)務(wù)流程(也稱為Sagas或流程管理器)的類,這些類負(fù)責(zé)協(xié)調(diào)不同聚合之間的業(yè)務(wù)邏輯。為了維護(hù)系統(tǒng)數(shù)據(jù)和狀態(tài)的完整性、冪等性和事務(wù)性,它發(fā)出的命令的實(shí)際發(fā)送是各個(gè)持久化倉儲(chǔ)來實(shí)現(xiàn)的。由于控制反轉(zhuǎn)和依賴注入對(duì)消費(fèi)者隱藏了這些類型的詳細(xì)信息,所以它和系統(tǒng)的一些其他屬性會(huì)造成一點(diǎn)困難在回答一些表面上瑣碎的問題時(shí),比如:

  • 誰會(huì)發(fā)出或已發(fā)出了特定的命令或事件?
  • 什么樣的類處理特定的命令或事件?
  • 流程或聚合在哪里創(chuàng)建或持久化?
  • 什么時(shí)候發(fā)出與其他命令或事件相關(guān)的命令?
  • 為什么系統(tǒng)會(huì)這樣運(yùn)行?
  • 應(yīng)用程序的狀態(tài)如何由特定的命令改變?

由于應(yīng)用程序的依賴關(guān)系非常松散,許多傳統(tǒng)的代碼分析工具和方法要么變得不那么有用,要么完全沒用。

讓我們以RegistrationProcessManager作為示例,列出一些涉及到回答這些問題的啟發(fā)式內(nèi)容。

  • 打開RegistrationProcessManager.cs文件,注意,與許多流程管理器一樣,它有一個(gè)ProcessState枚舉。我們注意進(jìn)程的開始狀態(tài):NotStarted。接下來,我們要找到做下面事情之一的代碼:
    • 創(chuàng)建流程的新實(shí)例(流程在哪里創(chuàng)建或持久化?)
    • 初始狀態(tài)被更改為不同的狀態(tài)(狀態(tài)如何更改?)
  • 找到源代碼中出現(xiàn)上述任何一種情況或同時(shí)出現(xiàn)上述兩種情況的代碼位置。在本例中,它是RegistrationProcessManagerRouter類中的Handle方法。重要提示:這并不一定意味著該流程是一個(gè)命令處理程序!流程管理器負(fù)責(zé)從存儲(chǔ)中創(chuàng)建和檢索聚合根(AR),以便將消息路由到AR,因此盡管它們的方法在名稱和簽名上與ICommandHandler實(shí)現(xiàn)類似,但它們并不實(shí)現(xiàn)處理命令的邏輯。
  • 請(qǐng)注意當(dāng)狀態(tài)發(fā)生變化時(shí)接收到的消息類型是作為方法參數(shù)被傳入的,因此我們現(xiàn)在需要找出消息的來源。
    • 我們還將注意到,RegistrationProcessManager發(fā)出了一個(gè)新的命令:MakeSeatReservation。
    • 如上所述,這個(gè)命令實(shí)際上不是由發(fā)出它的進(jìn)程發(fā)出的,相反,是當(dāng)進(jìn)程保存到磁盤時(shí),才會(huì)發(fā)出。
    • 對(duì)于其他任何作為進(jìn)程處理命令的副作用的,被發(fā)出的命令,需要一定程度的重復(fù)這些啟發(fā)。
  • 查找OrderPlaced的引用,找到一個(gè)或多個(gè)頂部(外部)組件,這些組件通過ICommandBus接口上的Send方法發(fā)出該類型的消息。
    • 由于內(nèi)部發(fā)出的命令是在倉儲(chǔ)的Save方法里,所以可以安全地假設(shè)直接調(diào)用Send方法的任何非基礎(chǔ)設(shè)施邏輯都是外部入口點(diǎn)。

    雖然啟發(fā)式的內(nèi)容肯定比這里所提到的要多,但是這里的這些內(nèi)容很可能足夠證明了。即使討論交互也是一個(gè)相當(dāng)漫長、繁瑣的過程。這很容易造成誤解。您可以通過這種方式理解各種命令/事件消息傳遞交互,但是這種方式不是很有效。

    備注:一般來說,一個(gè)人在任何時(shí)候都只能在腦子里保持四到八個(gè)不同的想法。為了說明這一概念,讓我們保守地計(jì)算一下你需要在短期記憶中同時(shí)保持的東西的數(shù)量,同時(shí)遵循上面的啟發(fā): 進(jìn)程類型+進(jìn)程狀態(tài)屬性+初始狀態(tài)(NotStarted) + new()的位置+消息類型+中間路由類類型+ 2 *N^ N命令發(fā)出(位置、類型、步驟)+判別規(guī)則(邏輯也是數(shù)據(jù)!) > 8

    當(dāng)基礎(chǔ)設(shè)施需求混合到等式中時(shí),信息飽和的問題會(huì)變得更加明顯。作為我們都是有能力的開發(fā)人員(對(duì)吧?),我們可以開始尋找方法來優(yōu)化這些步驟,并提高相關(guān)信息的信噪比。

    總之,我們有兩個(gè)問題:

    • 我們被迫記在腦子里的東西太多,無法有效理解。
    • 用于消息傳遞交互的討論和文檔冗長、容易出錯(cuò)且復(fù)雜。

    幸運(yùn)的是,使用MIL(消息傳遞中間語言)可以一舉兩得。

    MIL一開始是一系列LINQPad腳本和代碼片段,我創(chuàng)建這些腳本和代碼片段是為了在回答問題時(shí)幫助處理所有事情。最初,這些腳本完成的所有工作都是通過一個(gè)或多個(gè)項(xiàng)目程序集反映并輸出各種類型的消息和處理程序。在與團(tuán)隊(duì)成員的討論中,很明顯其他人也遇到了與我相同的問題。在與模式和實(shí)踐團(tuán)隊(duì)成員進(jìn)行了幾次聊天和頭腦風(fēng)暴會(huì)議之后,我們提出了引入一種小型領(lǐng)域特定語言(DSL)的想法,該語言將封裝所討論的交互。暫時(shí)命名為SawMIL toolbox,它位于http://jelster.github.com/CqrsMessagingTools/,它提供了實(shí)用工具、腳本和示例,使您能夠?qū)IL用作開發(fā)和分析流程管理器的一部分。

    在MIL中,消息傳遞組件和交互以特定的方式表示:命令(因?yàn)樗鼈兪窍到y(tǒng)執(zhí)行某些操作的請(qǐng)求)用?表示,比如DoSomething?。事件表示系統(tǒng)中發(fā)生的確定的事情,因此獲得一個(gè)!后綴,如SomethingHappened!

    MIL的另一個(gè)重要元素是消息發(fā)布和接收。從消息源(如Azure服務(wù)總線、NServiceBus等)接收的消息總是在前面加上“->”符號(hào),為了讓示例暫時(shí)保持簡(jiǎn)單,有一個(gè)可選的nil元素(句號(hào).)。用于顯式地指示no-op(換句話說,沒有接收到任何消息)。下面的代碼片段展示了nil元素語法的一個(gè)例子:

    SendCustomerInvoice? -> . CustomerInvoiceSent! -> .

    一旦發(fā)布了命令或事件,就需要對(duì)其進(jìn)行處理。命令只有一個(gè)處理程序,而事件可以有多個(gè)處理程序。MIL通過將處理程序的名稱放在消息傳遞操作的另一側(cè)來表示消息與處理程序之間的這種關(guān)系,如下面的代碼片段所示:

    SendCustomerInvoice? -> CustomerInvoiceHandler CustomerInvoiceSent! ->-> CustomerNotificationHandler-> AccountsAgeingViewModelGenerator

    注意,命令和命令處理程序位于同一行,是因?yàn)槊詈兔钐幚沓绦蚴?對(duì)1的。事件因?yàn)榭赡苡卸鄠€(gè)事件處理程序,所以把他們放到多行上。

    聚合根以@符號(hào)作為前綴,使用過twitter的人都會(huì)很熟悉它。聚合根從不處理命令,但偶爾可能處理事件。聚合根是最常見的事件源,它引發(fā)事件以響應(yīng)在聚合上調(diào)用的業(yè)務(wù)操作。但是,關(guān)于這些事件應(yīng)該清楚的一點(diǎn)是,在大多數(shù)系統(tǒng)中,有其他元素決定并實(shí)際執(zhí)行領(lǐng)域事件的發(fā)布。這是一個(gè)有趣的案例,其中業(yè)務(wù)和技術(shù)需求模糊了邊界,由基礎(chǔ)設(shè)施邏輯而不是應(yīng)用程序或業(yè)務(wù)邏輯來滿足需求。旅程代碼就是一個(gè)例子:為了確保事件源和事件訂閱者之間的一致性,持久化聚合根的存儲(chǔ)庫的實(shí)現(xiàn)才是負(fù)責(zé)將事件實(shí)際發(fā)布到總線的。下面的代碼片段顯示了AggregateRoot語法的一個(gè)示例:

    SendCustomerInvoice? -> CustomerInvoiceHandler @Invoice::CustomerInvoiceSent! -> .

    在上面的示例中,一個(gè)名為Scope上下文操作符的新語言元素出現(xiàn)在@AggregateRoot旁邊。范圍上下文元素由雙冒號(hào)(::)表示,它的兩個(gè)字符之間可能有空格,也可能沒有空格,用于標(biāo)識(shí)兩個(gè)對(duì)象之間的關(guān)系。上面,聚合根 '@Invoice'生成CustomerSent!事件來響應(yīng)CustomerInvoiceHandler調(diào)用的邏輯。下一個(gè)例子演示了在聚合根上使用Scope元素,它生成多個(gè)事件來響應(yīng)單個(gè)命令:

    SendCustomerInvoice? -> CustomerInvoiceHandler @Invoice::CustomerInvoiceSent! -> .:InvoiceAged! -> .

    Scope上下文還用于表示不涉及基礎(chǔ)設(shè)施消息傳遞設(shè)備的元素內(nèi)路由:

    SendCustomerInvoice? -> CustomerInvoiceHandler @Invoice::CustomerInvoiceSent! ->-> InvoiceAgeingProcessRouter::InvoiceAgeingProcess

    我將介紹的最后一個(gè)元素是State Change。狀態(tài)變化是跟蹤系統(tǒng)中發(fā)生的事情的最好方法之一,因此MIL將它們視為一等公民。這些語句必須出現(xiàn)在它們自己的文本行中,并以“*”字符作為前綴。這是MIL中唯一一次提到或出現(xiàn)任務(wù),因?yàn)樗浅V匾?下面的代碼片段顯示了State Change元素的一個(gè)例子:

    SendCustomerInvoice? -> CustomerInvoiceHandler @Invoice::CustomerInvoiceSent! ->-> InvoiceAgegingProcessRouter::InvoiceAgeingProcess*InvoiceAgeingProcess.ProcessState = Unpaid

    總結(jié)

    我們剛剛介紹了在松耦合應(yīng)用程序中描述消息傳遞交互時(shí)使用的基本步驟。盡管所描述的交互只是可能交互的子集,但是MIL正在發(fā)展成為一種簡(jiǎn)潔地描述基于消息的系統(tǒng)交互的方法。不同的名詞和動(dòng)詞(元素和動(dòng)作)由不同的、有記憶意義的符號(hào)表示。這提供了一種跨基板(粘糊糊的人腦< - >硅CPU)的方法來通信有關(guān)整個(gè)系統(tǒng)的有意義的信息。盡管該語言很好地描述了某些類型的消息傳遞交互,但它仍然是一項(xiàng)正在進(jìn)行的工作,需要開發(fā)或改進(jìn)該語言的許多元素和工具。這提供了一些很好的機(jī)會(huì)去為OSS貢獻(xiàn)代碼,如果你一直在觀望或思考參與OSS去貢獻(xiàn)代碼,沒有時(shí)間猶豫了,現(xiàn)在就去http://jelster.github.com/CqrsMessagingTools/,fork倉庫,馬上開始吧!

    總結(jié)

    以上是生活随笔為你收集整理的上下伸缩代码_CQRS之旅——旅程4(扩展和增强订单和注册限界上下文)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。