到 2015 2015年 7 月

30 卷數 7

切削刃-cqrs 體系和基於消息的應用程式

Dino Esposito |到 2015 2015年 7 月

Dino Esposito在年底的一天,命令和查詢職責分離 (CQRS) 是軟體分離式設計,改變狀態碼和代碼,只是讀取的狀態。分離可以將邏輯和基於不同圖層上。它也可以是物理的和涉及截然不同。沒有宣言,也沒有背後 CQRS 的時尚理念。唯一的司機是設計的其簡約。這些瘋狂的日子壓倒性業務複雜性簡化的設計是唯一安全的方式,以確保效率、 優化和成功。

我的最後一列 (msdn.microsoft.com/magazine/mt147237) 提供一個視角 CQRS 的辦法,使它適用于任何類型的應用程式。一旦你考慮使用 CQRS 架構和截然不同的指揮和查詢堆疊,你開始思考的方法分別優化每個堆疊。

不再有使某些操作風險、 不切實際或也許只是過於昂貴的模型約束。該系統的願景成為更多面向任務的。更重要的是,它會發生作為一個自然的過程。甚至一些領域驅動設計的概念,如聚合停止看起來如此令人厭煩。甚至他們在設計中找到自己自然的位置。這是簡化設計的力量。

如果你現在很好奇 CQRS 才開始尋找案例研究和接近您的商務應用程式,您可能會發現大多數引用引用使用事件和消息模型和實現業務邏輯的應用程式方案。雖然 CQRS 可以愉快地支付帳單支付與遠為簡單的應用程式 — — 那些否則你可能的標籤,作為普通的 CRUD 應用程式 — — 它絕對閃耀在的情況下更大的業務複雜性。從那,你可以推斷出更複雜的商務規則和改變的高傾向。

基於消息的體系結構

同時觀察真實的世界,你會看到在過程和結果從這些行動的事件行動。行動和事件進行資料有時會產生新的資料,和那是重點。它僅僅是資料而已。你不需要一個成熟的物件模型支援執行這些操作。物件模型還可以會有用。你會看到在一個時刻,雖然,它是組織業務邏輯只是另一種可能的選擇。

一種基於消息的體系結構是有益的因為它極大地簡化了管理複雜、 複雜和頻繁變更的業務工作流程。這些類型的工作流包括依賴于遺留代碼、 外部服務和動態變化規律。然而,建立一個基於消息的架構將保持命令和查詢堆疊整齊的 CQRS 的上下文之外幾乎不可能分離。因此,您可以使用以下體系結構為唯一命令堆疊。

消息可以是某一命令或事件。在代碼中,您通常會定義一個基類的消息並從那,定義附加基類的命令和事件,如中所示圖 1

圖 1 定義基本郵件類

public class Message
{
  public DateTime TimeStamp { get; proteted set; }
  public string SagaId { get; protected set; }
}
public class Command : Message
{
  public string Name { get; protected set; }
}
public class Event : Message
{
  // Any properties that may help retrieving
  // and persisting events.
}

從語義的角度來看,命令和事件是略有不同的實體和服務于不同但相關的目的。事件是與 Microsoft.NET 框架幾乎相同:一個類進行資料,並通知你的事情時已發生。命令是針對使用者或一些其他系統元件要求的後端執行的操作。事件和命令遵循相當標準的命名約定。命令則必須像 SubmitOrderCommand,事件在過去時態,如 OrderCreated。

通常情況下,按一下任何介面元素起源的命令。一旦系統接收到命令,它起源的任務。任務可以是任何從一個長時間運行的有狀態過程、 單個操作或無國籍的工作流。一個共同的名字,對於這樣一項任務是傳奇。

任務是單向,所得下來通過中介軟體和可能最終會修改系統和存儲狀態的演示文稿。命令不通常資料返回到演示文稿中,除了也許某種快速形式的回饋,例如是否操作已成功完成或原因它失敗。

顯式使用者操作不會觸發命令的唯一途徑。你還可以放置命令以非同步方式與系統交互的自主服務。試想,B2B 情況,如運輸的產品,在夥伴之間的通信發生在 HTTP 服務。

一種基於消息的體系結構中的事件

所以命令起源任務和任務通常由結合起來,形成一個工作流的幾個步驟組成。往往當一個給定的步驟執行,結果通知應傳遞給其他元件,為額外的工作。子任務引發的命令鏈可以長和複雜。一種基於消息的體系結構是有益的因為它可以讓你模型 (由命令觸發) 的個體行為和事件的工作流。通過定義的命令和隨後發生的事件處理常式元件,您可以建模,任何複雜的業務流程。

更重要的是,你可以按照那樣經典的流程圖工作隱喻。這極大地簡化理解規則,並優化了與領域專家溝通。此外,由此產生的工作流是分成無數小的處理常式,每個執行的一小步。每一步也地方非同步命令和通知其他的事件的攔截器。

這種方法的一個主要好處是,應用程式邏輯是易於修改和擴展。你做的就是寫新部件並將它們添加到系統中,和你可以完全肯定地,他們不會影響現有代碼和現有的工作流。若要查看為什麼這是真的,究竟是如何工作,我將回顧一些基於消息的體系結構,包括一個新的基礎設施要素的實施細節 — — 公共汽車。

歡迎來到公車

一開始,我會看看手工匯流排元件。一輛公共汽車的核心介面被匯總如下:

public interface IBus
{
  void Send<T>(T command) where T : Command;
  void RaiseEvent<T>(T theEvent) where T : Event;
  void RegisterSaga<T>() where T : Saga;
  void RegisterHandler<T>();
}

通常情況下,公車是單身人士。它接收請求執行命令和事件通知。公車實際上不做任何具體的工作。它只是選擇註冊的元件來處理該命令或處理的事件。公共汽車持有的已知的業務進程觸發命令和事件,或提出其他命令的清單。

處理命令和相關的事件的過程通常被稱為傳奇。初始的公車在配置期間,您註冊處理常式和傳奇的元件。只是簡單類型的傳奇和表示一次性操作處理常式。此操作請求時,它開始和結束沒有被連結到其他事件或通過向公車推其他命令。 圖 2 持有的傳說和處理常式可能匯流排類實現引用在記憶體中的禮物。

圖 2 匯流排類實現的示例

public class InMemoryBus : IBus
{
  private static IDictionary<Type, Type> RegisteredSagas =
    new Dictionary<Type, Type>();
  private static IList<Type> RegisteredHandlers =
    new List<Type>();
  private static IDictionary<string, Saga> RunningSagas =
    new Dictionary<string, Saga>();
  void IBus.RegisterSaga<T>() 
  {
    var sagaType = typeof(T);
    var messageType = sagaType.GetInterfaces()
      .First(i => i.Name.StartsWith(typeof(IStartWith<>).Name))
      .GenericTypeArguments
      .First();
    RegisteredSagas.Add(messageType, sagaType);
  }
  void IBus.Send<T>(T message)
  {
    SendInternal(message);
  }
  void IBus.RegisterHandler<T>()
  {
    RegisteredHandlers.Add(typeof(T));
  }
  void IBus.RaiseEvent<T>(T theEvent) 
  {
    EventStore.Save(theEvent);
    SendInternal(theEvent);
  }
  void SendInternal<T>(T message) where T : Message
  {
    // Step 1: Launch sagas that start with given message
    // Step 2: Deliver message to all already running sagas that
    // match the ID (message contains a saga ID)
    // Step 3: Deliver message to registered handlers
  }
}

將命令發送到匯流排時,它經歷了一個三步驟的過程。第一,公共汽車檢查註冊的傳奇故事,看看是否配置為在收到該資訊時啟動任何註冊的傳奇清單。如果是這樣,一個新的佐賀元件具現化、 傳遞消息並添加到運行傳奇的清單。最後,公車檢查看看是否有任何已註冊的處理常式感興趣的消息。

傳遞給公車事件是像命令一樣對待,傳遞給已註冊的攔截器。如果有關的業務場景,然而,它可能會到一些事件存儲記錄一個事件。事件存儲是跟蹤系統中的所有事件的平原的僅限附加資料存儲。使用記錄的事件改變了不少。你可以登錄事件僅用於跟蹤或使用,作為唯一的資料來源 (事件來源)。你甚至可以使用它來時仍用經典資料庫來保存最後已知實體狀態跟蹤資料實體的歷史記錄。

編寫一個傳奇元件

傳奇是一個元件,它聲明瞭以下資訊:命令或啟動業務流程的事件關聯與傳奇、 傳奇可以處理,命令清單和傳奇故事有關的事件的清單。傳奇類實現的介面,它聲明的命令和感興趣的事件。如 IStartWith 和 ICanHandle 介面的定義如下:

public interface IStartWith<T> where T : Message
{
  void Handle(T message);
}
public interface ICanHandle<T> where T : Message
{
  void Handle(T message);
}

這裡是簽名的一個示例示例傳奇類:

public class CheckoutSaga : Saga<CheckoutSagaData>,
       IStartWith<StartCheckoutCommand>,
       ICanHandle<PaymentCompletedEvent>,
       ICanHandle<PaymentDeniedEvent>,
       ICanHandle<DeliveryRequestRefusedEvent>,
       ICanHandle<DeliveryRequestApprovedEvent>
{
  ...
}

在這種情況下,這個傳奇表示一個線上商店的結帳過程。佐賀的開始當使用者按一下簽出按鈕和應用程式層推到公車的簽出命令。 佐賀建構函式生成唯一的 ID,要處理好同一個業務流程的併發實例。你應該能夠處理多個併發運行結帳傳奇。ID 可以是 GUID,一個唯一的值發送命令請求或甚至會話 id。

傳奇,處理命令或事件包括從內部匯流排元件調用的 ICanHandle 或 IStartWith 的介面上的處理方法。在處理方法中,傳奇執行計算或資料的訪問。它然後過帳到其他聽力的傳奇故事的另一個命令,或只是激發作為通知的事件。例如,想像簽出工作流中所示圖 3

簽出工作流
圖 3 簽出工作流

佐賀的執行到接受付款交單的所有步驟。在這一點上,它將推接受­PaymentSaga 繼續到匯流排的支付命令。付款­傳奇將運行和火 PaymentCompleted 或 PaymentDenied 的事件。這些事件再一次將由 CheckoutSaga 處理。那傳奇將然後推進到交付步驟與安置反對與航運的合作夥伴公司的外部子系統進行交互的另一個傳奇的另一個命令。

串聯的命令和事件保持佐賀的活,直到它到達完成。在這方面,你可以看作一個傳奇經典工作流的開始點和結束點。另一件事要注意是一個傳奇是通常宿存。乘公共汽車通常處理持久性。 這裡介紹的公車類示例不支援持久性。商業的匯流排,如 NServiceBus 或甚至開源,像情勢,公車可能會使用SQL Server。為持續發生,你必須給每個傳奇實例一個唯一的 ID。

總結

要真正有效的現代應用程式,他們必須能夠與業務需求規模。基於消息的體系結構使它非常易於擴展和修改業務工作流程和支援新方案。您可以管理擴展完全隔離,所需的只添加一個新的佐賀或一個新的處理常式,註冊與在應用程式啟動時巴士和它讓它知道如何處理的郵件需要處理。只有當它是時間和將工作與系統的其餘部分時,將自動調用新的元件。它是簡單、 簡單、 有效。


Dino Esposito 合著的"Microsoft.NET:構建企業應用程式"(微軟出版社,2014年) 和"程式設計ASP.NETMVC 5"(微軟出版社,2014年)。Microsoft.NET 框架和 Android 平臺,它也會和經常在世界各地的業內活動中發表演講的技術福音傳教士,埃斯波西托共用軟體在他視覺 software2cents.wordpress.com 和在 Twitter 上 twitter.com/despos

感謝以下技術專家對本文的審閱:喬恩 · Arne Saeteras