本文章是由機器翻譯。

資料點

在領域驅動設計定界模型間的資料共用模式

Julie Lerman

下載代碼示例

Julie Lerman整個程式設計的一生中,可重用的代碼和可重用的資料一直駕駛的目的。所以,當我開始學習有關領域驅動設計 (DDD),我糾結其指導跨其有界的上下文,則可能會強制執行這種分離重複的代碼,即使重複的資料。我有一個小的適合當一些最優秀的人才在 DDD 世界試圖干預,以説明我看看我的舊方式的潛在陷阱。最後,EricEvans 解釋過,要選擇在哪裡要付出代價的複雜性。因為 DDD 是降低了軟體的複雜性,結果是你付出的代價在保持重複模型方面,可能重複的資料。

在本專欄中,我曾寫過 DDD 概念和他們"收縮 EF 模型與 DDD 界語境"在與資料驅動的經驗,第一次在 2013 年 1 月專欄中,對齊 (bit.ly/1isIoGE),然後在三部分的系列中稱為"編碼域驅動設計:提示 Data-Focused 開發者"開始在 bit.ly/XyCNrU。在本系列的第一,你可以找到一節題為"共用資料可以詛咒在複雜系統中。看看它的一些額外的見解,為什麼我會在這裡演示的方法非常有用。

我已經問了無數次如何,確切地說,您可以共用有界情況下,如果你遵循一個更極端的 DDD 模式,這是讓每個有界的上下文綁定到其自己的資料庫之間的資料。SteveSmith,並談到這在我們的領域驅動設計基礎課程 Pluralsight.com (bitly.com/PS-DDD),但我們實際上沒有執行,這樣做是有點更先進比那特定的課程的重點。

那裡有很多不同的方式,充分利用常見資料跨界的上下文。在本專欄中我要去焦點在一種情況特別:鏡像資料從一個系統到另一個地方的第一個系統專為編輯該資料,第二隻是需要一點資料的唯讀存取權限。

我要首先佈置的基本模式,然後再添加一些額外的詳細資訊。實施涉及大量的工作部件,包括控制反轉 (IoC) 容器和訊息佇列。如果你熟悉這些工具,執行應該是容易理解的。我不會去深入講述有關的政府間海洋學委員會和佇列的實現,但你將能夠查看和調試在配上這篇文章的下載示例。

示例場景:共用客戶清單

我選擇了一個非常簡單的場景,來證明這種模式。一個系統被竭誠為客戶服務。在這裡,使用者可以維護客戶資料,以及大量的其他資料。此系統進行交互的一種資料存儲機制,但這並不是重要的樣品。第二個系統被用於接受訂單。在該系統中,使用者需要訪問客戶,但是真的僅僅是為了確定客戶作出該命令。這意味著這種有界情況下需要只是客戶名稱和識別碼的唯讀清單。因此,連接到第二個系統的資料庫需要的客戶名稱的最新清單和基於保留在第一次系統中的客戶 Id。我選擇了來做到這一點的方法是在第二個系統鏡像這兩塊的資料保留在第一次系統中的每個客戶。

鏡像資料:高水準

在一個很高的水準,我會去申請的解決方案是客戶的每次客戶插入到系統中的 ID 和名稱應將添加到系統 B 資料存儲。如果在系統中更改現有客戶的名稱,然後系統 B 需要有正確的名稱,以及因此改名應導致更新系統 B 資料存儲區中。此域並不會刪除資料,但未來的增強可能會從系統 B 資料存儲中刪除非活動的客戶。我不會理會,在此實現中。

所以,前款規定的從我在乎我需要作出反應的系統 A 中只有兩個事件:

  • 客戶被插入
  • 更新現有的客戶名稱

在已連接的系統中,系統 B 可能暴露系統 A 調用,例如 InsertCustomer 或 UpdateCustomerName 的方法。或者,系統 A 可以引發事件,如 CustomerCreated 和 CustomerNameUpdated,對於其他的系統,包括系統 B,以捕捉並回應。

在回應每個事件,系統 B 需要做某事在其資料庫中。

因為這些系統已中斷連線,更好的方法是使用發佈-訂閱模式。系統會將一個或多個事件發佈到某種類型的運算子。一個或多個系統然後訂閱該相同的運算子,等待特定事件並執行他們自己的行為以回應這些事件。

發佈-訂閱與 DDD 的原則,要求這兩個系統必須意識到彼此,並因此不談直接到另一個對齊。所以,相反,我會用一種稱為反腐敗層概念。每個系統將通過運算子,將兩者之間洗牌的消息進行通信。

此運算子是一個訊息佇列。系統會向佇列發送消息。系統 B 將從佇列中檢索消息。在我的示例中,我將會有只有一個訂閱伺服器 — — 系統 B — — 但也可能有許多訂閱伺服器。

消息在事件是什麼?

正在發佈的事件時,CustomerCreated 事件,系統將發送一條消息,說,"客戶已插入。這是客戶的標識和名稱。這是完整的消息除外,它代表中的資料,不漂亮的英語句子。有關發佈到訊息佇列的事件有趣的部分是發行者並不在乎什麼系統檢索該消息或他們會做什麼反應。

系統 B 將通過插入或更新其資料庫中的客戶回應該消息。在現實中,系統 B 甚至不執行這一任務 ; 我會讓做這項工作服務。此外,我會讓決定如何執行更新的資料庫。在這種情況下,系統 B 資料庫將執行客戶使用其邏輯刪除原始的客戶記錄,並插入一個新的存儲的過程的"更新"。因為我將使用 Guid 作為標識鍵,將正確地維護客戶的身份。我不想為資料庫生成的金鑰,在我的 DDD 軟體而煩惱。預先創建的 Guid vs。 資料庫遞增的鍵是一個棘手的話題。你會需要定義您的邏輯,以配合您的公司資料庫的做法。

在結束時,系統 B,訂購的系統中,將有它可以使用的客戶的完整清單。進一步沿在該工作流,如果訂購系統需要更多資訊關於特定客戶,例如信用卡資訊或當前的送貨位址,我可以利用其他機制,例如調用服務,檢索該資料,如需要。但是我不會和在這裡,該工作流處理。

與訊息佇列進行通信

一個系統,讓您的溝通消息以非同步方式呼叫事件匯流排。 事件匯流排包含存儲消息,並將它們提供給誰需要檢索他們的基礎設施。它還提供一個 API 為與它交互。我將重點介紹一種特定實現發現是一種容易的方法,若要開始使用這種風格的通信:訊息佇列。當地有很多可供選擇的訊息佇列。在 DDD 基礎課上 Pluralsight.com,Smith,並選擇要作為我們的訊息佇列使用SQL Server服務代理。因為我們倆都工作與SQL Server,它為我們樹立簡單,我們只需要編寫 SQL 將推送郵件到佇列檢索它們。

寫這篇文章,決定它的時候我要學會使用的更受歡迎的開源訊息佇列,RabbitMQ 之一。這意味著安裝 RabbitMQ 伺服器 (和 Erlang !) 到我的電腦,以及拉 RabbitMQ.NET 用戶端中,所以我的應用程式可以方便地編碼反對它。你可以瞭解更多關於在 RabbitMQ rabbitmq.com。我還發現 RabbitMQ.NET 開發人員課程上 Pluralsight.com 是非常有説明。

系統 A,因此,具有將消息發送到 RabbitMQ 伺服器的機制。但系統 B,訂單系統,任何這種交互無關。系統 B 只是預計客戶清單,必須在資料庫中,並不在乎它是怎麼。一個單獨的小視窗服務將處理檢查 RabbitMQ 訊息佇列中的消息和相應地更新訂單系統資料庫。圖 1 顯示視覺化的工作流的整個過程。

訊息佇列允許陌生的制度,以分享資訊,在這種情況下,更新系統 B 資料庫
圖 1 訊息佇列允許陌生的制度,以分享資訊,在這種情況下,更新系統 B 資料庫

向佇列發送消息

會在與客戶類系統中所示的 A 圖 2。為了一個簡單的例子,它包括很少的屬性 — — ID、 名稱、 客戶和一些日誌記錄日期的來源。繼 DDD 模式,該物件具有內置的約束,以防止隨機編輯。您創建一個新的客戶,使用創建的工廠方法。如果你需要修復的名稱,則使用 FixName 方法。

圖 2 中客戶維修樓宇方面的客戶類

public static Customer Create(string name, string source) {
   return new Customer(name, source);
  }
  private Customer(string name, string source){
    Id = Guid.NewGuid();
    Name = name;
    InitialDate = DateTime.UtcNow;
    ModifiedDate = DateTime.UtcNow;
    Source = source;
    PublishEvent (true);
  }
  public Guid Id { get; private set; }
  public string Name { get; private set; }
  public DateTime InitialDate { get; private set; }
  public DateTime ModifiedDate { get; private set; }
  public String Source { get; private set; }
  public void FixName(string newName){
    Name = newName;
    ModifiedDate = DateTime.UtcNow;
    PublishEvent (false);
  }
  private void PublishEvent(bool isNew){
    var dto = CustomerDto.Create(Id, Name);
    DomainEvents.Raise(new CustomerUpdatedEvent(dto, isNew));
 }}

注意到建構函式和 FixName 方法調用 PublishEvent 方法,其中,反過來,創建簡單的 CustomerDto (其唯一 Id 和名稱的屬性),然後使用 DomainEvents 類從 Udi 大寒 2009 年 MSDN 雜誌文章,"雇用域模型模式"(msdn.microsoft.com/magazine/ee236415) 提出一個新的客戶­UpdatedEvent (見圖 3)。在我的示例中,我出版中回應簡單操作的事件。在實際的實現中,您可能希望將這些事件後到系統 A 資料庫成功地持久化了的資料發佈。

圖 3 類,用於封裝事件時更新客戶

public class CustomerUpdatedEvent : IApplicationEvent{
  public CustomerUpdatedEvent(CustomerDto customer, 
   bool isNew) : this(){
    Customer = customer;
    IsNew = isNew;
  }
  public CustomerUpdatedEvent()
  {
    DateTimeEventOccurred = DateTime.Now;
  }
  public CustomerDto Customer { get; private set; }
  public bool IsNew { get; private set; }
  public DateTime DateTimeEventOccurred { get; set; }
  public string EventType{
    get { return "CustomerUpdatedEvent"; }
 }}

CustomerUpdatedEvent 包的所有我需要知道關於此事件:隨著一個標誌,指示客戶是否是新 CustomerDto。也是將一個泛型事件處理常式所需的中繼資料。

CustomerUpdatedEvent 然後可以由我在我的應用程式中定義的一個或多個處理常式處理。我已經定義了只有一個處理常式,名為 CustomerUpdatedService 的服務:

public class CustomerUpdatedService : IHandle<CustomerUpdatedEvent>
{
  private readonly IMessagePublisher _messagePublisher;
  public CustomerUpdatedService(IMessagePublisher messagePublisher){
    _messagePublisher = messagePublisher;
  }
  public void Handle(CustomerUpdatedEvent customerUpdatedEvent){
    _messagePublisher.Publish(customerUpdatedEvent);
}}

這項服務將處理所有的 CustomerUpdatedEvent 實例通過使用指定的消息發行者發佈事件引發我的代碼。我沒有指定發佈伺服器在這裡 ; 我只引用抽象概念時,IMessagePublisher。我,我雇用了 IoC 模式,讓我松耦合我的邏輯。我是一個善變的 gal。 今天我可能要一個消息發行者發佈。明天,我可能更喜歡另一個。在後臺,我用 StructureMap (structuremap.net),一個流行的工具.NET 開發人員對.NET 應用程式中管理國際奧會。StructureMap 讓我說明在哪裡可以找到處理由 DomainEvents.Raise 引發事件的類。 StructureMap 的作者JeremyMiller,在 MSDN 雜誌稱為"實踐中的模式"中寫道: 優秀的系列相關的模式,在此示例中的應用 (bit.ly/1ltTgTw)。使用 StructureMap,我配置我的應用程式,要知道當它看到 IMessagePublisher,它應該使用具體的類,RabbitMQMessagePublisher,其邏輯如下所示:

public class RabbitMqMessagePublisher : IMessagePublisher{
  public void Publish(Shared.Interfaces.IApplicationEvent applicationEvent) {
    var factory = new ConnectionFactory();
    IConnection conn = factory.CreateConnection();
    using (IModel channel = conn.CreateModel()) {
      [code to define the RabbitMQ channel]
      string json = JsonConvert.SerializeObject(applicationEvent, Formatting.None);
      byte[] messageBodyBytes = System.Text.Encoding.UTF8.GetBytes(json);
      channel.BasicPublish("CustomerUpdate", "", props, messageBodyBytes);
 }}}

請注意我已經刪除特定的配置 RabbitMQ 代碼的行數。你可以看到在你下載的完整清單 (msdn.microsoft.com/magazine/msdnmag1014)。

這種方法的肉是它發佈到佇列的事件物件的 JSON 表示。這裡是該字串時我已經添加了一個新的客戶,命名為Julie Lerman的樣子:

{
"Customer":
  {"CustomerId":"a9c8b56f-6112-42da-9411-511b1a05d814",
    "ClientName":"Julie Lerman"},
"IsNew":true,
"DateTimeEventOccurred":"2014-07-22T13:46:09.6661355-04:00",
"EventType":"CustomerUpdatedEvent"
}

在此消息公佈後,客戶維護系統的參與是完整的。

在我的應用程式範例,我用一組測試導致消息發佈到佇列中,如中所示圖 4。寧願比生成測試,將檢查佇列,當他們幹完活,我只是流覽到 RabbitMQ 管理器在我的電腦上,並使用其工具。請注意,測試建構函式中初始化一個類名為國際奧會。這是在那裡我已經配置了 StructureMap 連接起來的 IMessagePublisher 和我的事件處理常式。

圖 4 發佈到測試中的 RabbitMq

[TestClass]
public class PublishToRabbitMqTests
{
  public PublishToRabbitMqTests()
  {IoC.Initialize();
  }
  [TestMethod]
  public void CanInsertNewCustomer()
  {
    var customer = Customer.Create("Julie Lerman", 
      "Friend Referral");
    Assert.Inconclusive("Check RabbitMQ Manager for a message re this event");
  }
  [TestMethod]
  public void CanUpdateCustomer() {
    var customer = Customer.Create("Julie Lerman", 
      "Friend Referral");
    customer.FixName("Sampson");
    Assert.Inconclusive("Check RabbitMQ Manager for 2 messages re these events");
}}

檢索消息和更新訂單系統資料庫

在 RabbitMQ 伺服器上等待要檢索擱置的消息。這一任務由 Windows 服務運行不斷,定期輪詢佇列的新消息。當它看到一條消息時,該服務檢索並處理它。當他們過來,也可以由其它訂閱伺服器處理消息。為了此示例中,我創建了一個簡單的主控台應用程式,而不是一種服務。這讓我輕鬆運行和調試從Visual Studio的"服務"而學習。為下一次反覆運算,可能查閱那微軟 Azure WebJobs (bit.ly/1l3PTYH),而不是糾纏的 Windows 服務或使用我的主控台中的應用技巧。

該服務採用類似的模式,提高與大寒的 DomainEvents 類的事件、 回應處理常式類中的事件和初始化一個國際奧會類,使用 StructureMap 來查找的事件處理常式。

這項服務,RabbitMQ 偵聽使用 RabbitMQ.NET 用戶端訂閱類的消息而定。在下面的輪詢方法中,消息保持偵聽的 _subscription 物件,可以為此參見邏輯。每次檢索到消息,它反序列化的 JSON 我回 CustomerUpdatedEvent 存儲在佇列中,然後將引發的事件:

private void Poll() {
  while (Enabled) {
    var deliveryArgs = _subscription.Next();
    var message = Encoding.Default.GetString(deliveryArgs.Body);
    var customerUpdatedEvent =
      JsonConvert.DeserializeObject<CustomerUpdatedEvent>(message);
    DomainEvents.Raise(customerUpdatedEvent);
}}

這項服務包含單個類,客戶:

public class Customer{
  public Guid CustomerId { get; set; }
  public string ClientName { get; set; }
}

當反序列化的 CustomerUpdatedEvent 時,其一部分的顧客財產 — — 最初由 CustomerDto 填充在客戶管理系統 — — 于此服務的客戶物件進行反序列化。

引發的事件發生了什麼是服務的最有趣的部分。這裡是類,CustomerUpdatedHandler,處理該事件:

public class CustomerUpdatedHandler : IHandle<CustomerUpdatedEvent>{
  public void Handle(CustomerUpdatedEvent customerUpdatedEvent){
    var customer = customerUpdatedEvent.Customer;
    using (var repo = new SimpleRepo()){
      if (customerUpdatedEvent.IsNew){
        repo.InsertCustomer(customer);
      }
      else{
        repo.UpdateCustomer(customer);
 }}}}

此服務使用Entity Framework(EF) 來與資料庫進行交互。圖 5 顯示相關的交互封裝在兩種方法 — — InsertCustomer 和 UpdateCustomer — — 簡單的存儲庫中。如果事件的 IsNew 屬性為 true,該服務調用存儲庫中的 InsertCustomer 方法。否則,它將調用 UpdateCustomer 方法。

圖 5 的 InsertCustomer 和 UpdateCustomer 方法

public void InsertCustomer(Customer customer){
  using (var context = new CustomersContext()){
    context.Customers.Add(customer);
    context.SaveChanges();
}}
public void UpdateCustomer(Customer customer){
  using (var context = new CustomersContext()){
    var pId = new SqlParameter("@Id", customer.CustomerId);
    var pName = new SqlParameter("@Name", customer.ClientName);
    context.Database.ExecuteSqlCommand      
      ("exec ReplaceCustomer {0}, {1}", 
        customer.CustomerId, customer.ClientName);
}}

這些方法執行使用 EF DbCoNtext 相關的邏輯。對於插入,它添加客戶,然後調用 SaveChanges。EF 將執行的資料庫插入命令。更新時,它將給客戶 id 和 CustomerName 存儲的過程,使用任何邏輯我 — — 或我信任的 DBA — — 定義了用於執行更新。

因此,這項服務要確保訂購系統中的客戶清單總是具有客戶最新名冊,作為保留在客戶維護系統中的資料庫中執行所需的工作。

是的這是很多層和拼圖 !

因為我使用一個簡單的範例展示此工作流,您可能想解決方案數量驚人的殺傷力。但請記住,這一點是這是如何編排的解決方案,當您使用 DDD 實踐來解決複雜的軟體問題。當聚焦在客戶維護域上,我不關心其他系統。通過與國際奧會、 處理常式和訊息佇列使用抽象,我可以滿足外部系統沒有污濁了域本身。Customer 類只引發事件。對於本演示中,這是最容易的地方,以確保工作流對讀者來說,有道理,但它可能已為您的域太泥濘。你肯定能提高從另一個地方事件在應用程式中,也許從一個存儲庫一樣難以將更改推入其自己的資料存儲。

下載此列的示例解決方案使用 RabbitMQ,並且需要在您的電腦上安裝其羽量級的開放源碼伺服器。我已經下載的讀我檔案中包含的引用。我還會在我的博客上張貼一段短視頻 thedatafarm.com,所以你可以看到我逐句通過代碼,檢查 RabbitMQ 管理器和資料庫以查看結果。


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 的-視頻

感謝以下的微軟技術專家對本文的審閱:塞薩爾 de la Torre