本文章是由機器翻譯。

資料點

在領域驅動設計定界模型間的資料共用模式,第二部分

Julie Lerman

下載代碼示例

Julie Lerman在 10 月 2014年資料點 (msdn.microsoft.com/magazine/dn802601),與每個 BC 獨立于其自己的資料庫,就一種模式為鏡像到另一個資料庫中的資料,當您使用多個領域驅動設計 (DDD) 有界上下文 (西元前),寫。該方案是客戶管理西元前允許使用者管理客戶資料,插入、 更新和刪除客戶詳細資訊。西元前二是訂購的系統,需要對兩個關鍵的客戶資訊的訪問:客戶的識別碼鍵和名稱。由於這些系統是在兩個單獨的 BCs,你不能達到從一個到另一個共用資料。

DDD 是為了解決複雜的問題,並簡化域中存在的問題往往意味著移動的域之外的複雜性。所以客戶管理西元前不需要知道這隨後共用的資料。在我以前的專欄中,我受雇的發佈/訂閱模式來解決這一問題,利用域事件、 訊息佇列 (RabbitMQ) 和服務。有大量的移動部件。

一個快捷方式,故意採取了為避免太多的概念的解釋是通過發佈到訊息佇列的事件引發的一系列事件直接從 customer 類。

這個月,我想以增強解決方案以兩種方式。首先,我想要在工作流中發佈消息從一個更符合邏輯的地方:第一次的確認後客戶 (無論是新的或修改的) 是否已經成功持久化到資料庫中的客戶系統。我也想確保只有在回應相關事件發佈的事件。發佈後創建新的客戶的事件有道理。但還要對付需要則可能要更合理地辨別時將更新發佈到訊息佇列的假設條件。在這種情況下,我想確保只有時客戶的名稱已更改發佈有關消息。如果其他資料被修改,不會影響客戶的名字,我不會發佈消息。

這兩個變化會使解決方案更適用于現實世界的方案。

創建或更新的客戶成為客戶堅持

當創建的客戶或客戶的名稱固定的當前解決方案提出了通知。建構函式和此 FixName 方法都調用 PublishEvent:

public void FixName(string newName){
    Name = newName;
    ModifiedDate = DateTime.UtcNow;
    PublishEvent(false);
  }

PublishEvent 觸發的工作流,結果被發佈到佇列的消息:

private void PublishEvent(bool isNew){
    var dto = CustomerDto.Create(Id, Name);
    DomainEvents.Raise(new CustomerUpdatedEvent(dto, isNew));
  }

您可以檢查 10 月列有關該解決方案的詳細資訊。而不是從引發事件的類,我想要引發事件後曉得了新客戶實例或修復給客戶名稱是否已經成功持久化到資料庫中。

這意味著刪除 PublishEvent 方法和對它從 Customer 類的調用。

我資料層都有一個包含客戶骨料的資料訪問邏輯的類。我搬的 PublishEvent 方法到此類重命名為 PublishCustomerPersistedEvent。在我堅持到資料庫的客戶的方法,我叫它 SaveChanges 完畢後新的事件 (見圖 1)。

圖 1 持久性類引發事件後持久化資料

public class CustomerAggregateRepository {
public bool PersistNewCustomer(Customer customer) {
  using (var context = new CustomerAggregateContext()) {
    context.Customers.Add(customer);
    int response = context.SaveChanges();
    if (response > 0) {
      PublishCustomerPersistedEvent(customer, true);
      return true;
    }
    return false;
  }
}
public bool PersistChangeToCustomer(Customer customer) {
  using (var context = new CustomerAggregateContext()) {
    context.Customers.Attach(customer);
    context.Entry(customer).State = EntityState.Modified;
    int response = context.SaveChanges();
    if (response > 0) {
      PublishCustomerPersistedEvent(customer, false);
      return true;
    }
    return false;
  }
}
   private void PublishCustomerPersistedEvent(Customer customer, 
     bool isNew) {
     CustomerDto dto = CustomerDto.Create(customer.Id, customer.Name);
     DomainEvents.Raise(new CustomerUpdatedEvent(dto, isNew));
   }
 }

與這一舉動,也需要移動到資料層專案發佈消息的基礎設施。圖 2 顯示有關專案 (客戶­Management.Core 和客戶­Management.Infastructure) 我為前一列旁邊後我搬到資料層的此事件的專案創建。CustomerUpdatedEvent、 DTO 和服務現在處於基礎設施專案。這是令人滿意的移動基礎結構邏輯域外。我曾一直需要在核心域中的代碼味道的困擾。

專案結構之前和之後移動事件發佈到資料層
圖 2 專案結構之前和之後移動事件發佈到資料層

我有兩個測試,我用來驗證成功插入和更新做,事實上,發佈正確的郵件排入佇列。第三個實驗驗證失敗的更新不會嘗試向佇列發佈任何消息。你可以看到這些本文附帶的下載解決方案中的測試。

在資料層中,不是在域模型

這是一個很簡單的改變,但這一舉動也掙扎 — — 不技術方面 — — 但在說明理由搬出我的 BC,走進我的資料層的事件。我有爭論這狗去散步。(它不是不正常的我散步,穿過我的森林,或我的道路,在自言自語。福爾­老頭兒,我住在一個安靜的地方,在那裡沒有人會質疑我的理智.)辯論結束了下述結論:在西元前不屬於出版事件。原因何在?因為在西元前只關心自己。西元前不在乎什麼其他的 BCs 或服務或應用程式想要或需要。所以,"我需要與訂購系統,分享我的持久化的資料"不是在西元前的擔憂不無道理。這不是一個域的事件,但事件相關的堅持,我會把它放在應用程式事件存儲桶中。

固定移動通過創建一個新的問題

這裡有一個問題進入我的持久層移動發佈事件所造成。PersistChangeToCustomer 方法用於保存到客戶實體,以及其他編輯操作。例如,客戶實體也有能力來添加或更新客戶的送貨和帳單位址。位址是值物件,創建或取而代之的一套新的值反映了給客戶的改變。

我需要這些位址之一更改時調用 PersistChangeToCustomer。但在這種情況下,還有沒有點在向說的客戶名稱已更改的佇列發送一條消息。

你是怎麼讓知道客戶名稱沒有更改此持久層的?一種本能的解決方案是委任標記屬性 (例如 NameChanged。但我不想要依靠添加布林值所需跟蹤的詳細的狀態。從客戶類中,但不是那種會觸發到佇列的另一條消息引發一個事件 .我不想要一條消息,只是說,Don不發送一條消息"。但如何捕獲的事件嗎?

再次,吉米 · 博加德來到另一個輝煌的解決辦法救援。他 2014 年 5 月的博客帖子,"A 更好域事件模式"(bit.ly/1vUG3sV),表明收集事件,而不是使他們立即上升,然後讓持久層抓住該集合並根據需要處理的事件。他是模式的要刪除靜態的 DomainEvents 類,不允許您控制當事件引發和因此導致的副作用。這是關於域事件思維較新線。我重構會不約而同地避免這一問題,但我不可否認仍然受靜態的 DomainEvents 類。一如往常,我將繼續學習與進化我的做法。

我愛博加德的做法,但我要去偷去的主意,並使用它比他執行略有不同。我不需要將消息發送到佇列 ; 我只需要讀取事件。它是一個偉大的方式來捕獲此事件中的客戶物件而無需創建各種隨機狀態標誌。例如,我可以避免尷尬,包含一個布林值,表示,"該名稱固定的"設置為 true 還是 false,為需要。

博加德使用集合 < IDomainEvent > 稱為族群的介面中的事件屬性的屬性。如果我在我的域中有多個實體,我會做同樣或也許將其添加到實體的基類。但在這個演示中,我要把新的屬性直接進入我的客戶物件。我創建一個私有欄位,只有客戶可以修改該集合暴露事件作為唯讀:

private readonly ICollection<IDomainEvent> _events;
public ICollection<IDomainEvent> Events {
  get { return _events.ToList().AsReadOnly(); }
}

接下來,我將定義相關的事件:CustomerNameFixedEvent,本專欄的第 1 部分中實現我使用的 IDomainEvent 介面。CustomerNameFixedEvent 並不需要太多。它將設置是介面的一部分的 DateTimeEventOccurred 屬性:

public class CustomerNameFixedEvent : IDomainEvent{
  public CustomerNameFixedEvent(){
    DateTimeEventOccurred = DateTime.Now;
  }
  public DateTime DateTimeEventOccurred { get; private set; }   }
}

現在,每當 Customer.FixName 的調用,我可以將此事件的一個實例添加到事件集合:

public void FixName(string newName){
  Name = newName;
  ModifiedDate = DateTime.UtcNow;
  _events.Add(new CustomerNameFixedEvent());
}

這給了我更鬆散耦合的而不是國家財產的東西。此外,我可以添加邏輯對它在未來隨著我的功能變數名稱的發展而無需修改我的客戶類的架構。我可以利用它在我的持久性方法。

PersistChangeToCustomer 方法現在具有新的邏輯。它將檢查傳入的客戶中的此事件。如果該事件存在,它會發射到佇列的消息。圖 3 在它顯示完整的方法,再用邏輯的新位 — — 一張支票給之前發佈的事件種類。

圖 3 PersistChangeToCustomer 方法檢查事件種類

public bool PersistChangeToCustomer(Customer customer) {
    using (var context = new CustomerAggregateContext()) {
      context.Customers.Attach(customer);
      context.Entry(customer).State = EntityState.Modified;
      int response = context.SaveChanges();
      if (response > 0) {
        if (customer.Events.OfType<CustomerNameFixedEvent>().Any()) {
          PublishCustomerPersistedEvent(customer, false);
        }
        return true;
      }
      return false;
    }
  }

該方法仍返回一個布林值顯示是否成功地保存客戶,由 SaveChanges 大於 0 回應表示。 如果是這樣,該方法會檢查是否有任何 CustomerNameFixedEvents 在 Customer.Events 中,發佈有關客戶一樣早些時候持久化的消息。如果由於某種原因 SaveChanges 失敗,最有可能將會表明由異常。然而,我也看的值作為回應,所以我可以從該方法返回 false。調用的邏輯將決定該怎麼辦的失敗 — — 也許試圖保存再次或將通知發送到最終使用者或其他系統。

因為我使用了Entity Framework(EF),值得注意的是您可以配置 EF6 重 SaveChanges 試瞬態連接錯誤。但是,將已經發揮得到 SaveChanges 的回應的時候。檢查我 2013 年 12 月的文章,Entity Framework6,忍者版"(msdn.microsoft.com/magazine/dn532202),關於 DbExecutionStrategy 的詳細資訊。

我添加了一個新的測試,以驗證新的邏輯。此測試創建和仍然存在的新客戶,然後編輯客戶的 BillingAddress 屬性,仍然存在這種變化。我的佇列接收消息創建新客戶的但它獲取沒有消息回應的位址更改的更新:

[TestMethod]
public void WillNotSendMessageToQueueOnSuccessfulCustomerAddressUpdate() {
  Customer customer = Customer.Create("George Jetson", "Friend Referral");
  var repo = new CustomerAggregateRepository();
  repo.PersistNewCustomer(customer);
  customer.CreateNewBillingAddress
    ("123 SkyPad Apartments", "", "Orbit City", "Orbit", "n/a", "");
  repo.PersistChangeToCustomer(customer);
  Assert.Inconclusive(@"Check status of RabbitMQ Manager for a create message,
    but no update message");
}

Stephen憶,審查這篇文章,表明"測試間諜模式"(xunitpatterns.com/Test Spy.html) 作為一種替代方式的驗證消息做排入佇列。

在兩個部分的解決方案

如何在有限的上下文共用資料是許多開發人員,瞭解 DDD 問一個問題。SteveSmith,並暗示這種能力,在我們的 Pluralsight 課程,領域驅動設計基本原理 (bit.ly/PS-DDD),但沒有證明它。我們已經被要求很多次,詳述如何把這事。在這個小小的系列的第一篇文章,利用了很多工具來構建一個工作流,將允許 1 下午的資料庫與資料庫使用不同的西元前共用中存儲的資料。精心策劃的發佈-訂閱模式使用訊息佇列、 事件和反演的控制項容器讓我實現的模式非常鬆散耦合的方式。

這個月,我擴大中答覆提出的問題的示例:何時它確實有意義,與 DDD 關注焦點,以發佈消息?最初,我引發的資料共用的工作流的發佈來自 Customer 類的事件,因為它創建新客戶或更新客戶的姓名。因為所有技工都到位,它很容易在這篇文章移動到存儲庫,允許我推遲發佈消息,直到我確信客戶資料的邏輯已經已經成功持久化到資料庫中。

還有總是更多微調工作要做,,也許不同的工具,你可能更願意使用。但現在,你應該有一個控制碼 (沒有雙關意,但我必須現在離開) 接近 DDD 模式允許有界的上下文處理他們自己獨立的資料庫。


Julie Lerman 是 Microsoft MVP,.NET 的導師和顧問,住在佛蒙特州的山。你可以找到她提出關於資料訪問和 other.NET 的主題,在使用者組和世界各地的會議。她的博客 thedatafarm.com/blog ,是作者的"程式設計Entity Framework"(2010 年),以及代碼第一版 (2011 年) 和 DbCoNtext 版 (2012 年),所有從 O'Reilly 媒體。跟著她在 Twitter 上 twitter.com/julielerman ,看看她的 Pluralsight 課程 juliel.me PS 的-視頻

感謝以下的微軟技術專家對本文的審閱:Stephen憶
目前技術傳福音和發展 (TED) 團隊內微軟公司首席軟體工程師,Stephen帶來他多樣 20-加-年的軟體和技術,協助他們通過尖端和預發佈 Microsoft 開發人員產品及技術的選擇 Microsoft 合作夥伴組織的經驗。