Il presente articolo è stato tradotto automaticamente.

Concetti sui dati

Modello per la condivisione di dati nei contesti vincolati per la progettazione basata sui domini

Julie Lerman

Scaricare il codice di esempio

Julie LermanPer la mia vita intera programmazione, codice riutilizzabile e dati riutilizzabili sono stati un obiettivo guida. Così, quando ho cominciato a imparare circa jealously Design (DDD), ho lottato con la sua guida per far rispettare tale separazione attraverso suoi contesti delimitati che il risultato potrebbe essere duplicato codice e anche duplicati dati. Ho avuto una piccola misura, quando alcune delle migliori menti del mondo DDD ha tentato un intervento per aiutarmi a vedere le potenziali insidie dei miei vecchi modi. Infine, Eric Evans ha spiegato che uno ha dovuto scegliere dove a pagare il prezzo della complessità. Perché DDD è ridurre la complessità del software, il risultato è che si paga un prezzo rispetto al mantenimento di modelli duplicati e possibilmente duplica i dati.

In questo articolo, ho scritto sui concetti DDD e come allineare con l'esperienza basata su dati, prima nel mio articolo di gennaio 2013, "Shrink EF modello con DDD contesti delimitata" (bit.ly/1isIoGE) e poi in una serie di tre parti chiamato "codifica per Domain-Driven Design: Consigli per gli sviluppatori di Data-Focused,"che inizia a bit.ly/XyCNrU. Nel primo di questa serie si può trovare una sezione intitolata "Shared dati possono essere una maledizione nei sistemi complessi". Dare un'occhiata per qualche informazione aggiuntiva per cui è utile l'approccio che illustrerò qui.

Mi è stato chiesto più volte come, esattamente, è possibile condividere dati tra contesti delimitati se seguite uno dei modelli più estremi DDD, che deve avere ogni contesto delimitata legata al proprio database. Steve Smith e ho parlato di questo nel nostro corso di fondamenti di progettazione jealously su Pluralsight.com (bitly.com/PS-DDD), ma non in pratica, come così facendo si è un po ' più avanzato rispetto al centro di quel particolare corso.

Ci sono diversi modi per sfruttare dati comuni in contesti delimitati. In questo articolo vado a concentrarsi su uno scenario in particolare: dati da un sistema a altro dove il primo sistema è stato progettato per la modifica di tali dati e il secondo ha solo bisogno di accedere in sola lettura per un po' di dati di mirroring.

Verrà innanzitutto delineare il modello di base, poi aggiungere qualche dettaglio aggiuntivo. L'implementazione implica un certo numero di parti del lavoro, tra cui contenitori di inversione del controllo (IoC) e le code di messaggi. Se si ha familiarità con questi strumenti, l'implementazione deve essere più facile da comprendere. Non voglio entrare nel dettaglio informazioni sull'implementazione di IoC e coda, ma sarete in grado di vedere ed eseguire il debug del campione di download che va con questo articolo.

Lo Scenario di esempio: Un elenco di clienti di condivisione

Ho scelto uno scenario molto semplice per dimostrare questo modello. Un sistema dedicato al servizio del cliente. Qui, gli utenti potranno gestire i dati dei clienti, insieme a un sacco di altri dati.  Questo sistema interagisce con un meccanismo di archiviazione di dati, ma che non è importante per il campione. Il secondo sistema è progettato per prendere ordini. In quel sistema, gli utenti devono accedere ai clienti, ma in realtà solo per identificare il cliente effettua l'ordine. Ciò significa che questo contesto delimitato ha bisogno solo una lista di nomi di clienti e identificatori di sola lettura. Pertanto, il database collegato al secondo sistema ha bisogno di una lista aggiornata dei nomi cliente e IDs basato sui clienti che sono mantenuti nel primo sistema. L'approccio che ho scelto di realizzare questo è specchio del sistema secondo quei due pezzi di dati per ogni cliente che viene mantenuto nel primo sistema.

Dati del mirroring: Alto livello

Un livello molto alto, la soluzione che potrai applicare è che ogni volta che un cliente è inserito nel sistema un, l'ID e il nome del cliente che devono essere aggiunte all'archivio di dati di sistema B. Se cambio il nome di un cliente esistente nel sistema A, poi B sistema deve avere il nome corretto, quindi un cambiamento di nome dovrebbe causare un aggiornamento nell'archivio dati del sistema B. Questo dominio non cancella i dati, ma potrebbe essere una futura valorizzazione rimuovere clienti inattivi da archivio dati del sistema B. Non mi preoccuperò con quello in questa implementazione.

Così, dal paragrafo precedente, mi interessa solo due eventi nel sistema A cui devo rispondere:

  • Un cliente è stato inserito
  • Nome di un cliente esistente è stato aggiornato

In un sistema connesso, il sistema B potrebbe esporre metodi per sistema A chiamare, ad esempio InsertCustomer o UpdateCustomerName. O sistema un potrebbe generare eventi, come CustomerCreated e CustomerNameUpdated, per altri sistemi, compreso il sistema B, per intercettare e rispondere.

In risposta a ogni evento, sistema B deve fare qualcosa nel proprio database.

Perché questi sistemi sono scollegati, un approccio migliore è di impiegare una pubblicazione-sottoscrizione modello. Sistema un pubblicherà uno o più eventi a qualche tipo di operatore. E uno o più sistemi quindi sottoscrivere tale operatore stesso, in attesa di eventi particolari e di compiere le proprie azioni in risposta a quegli eventi.

Pubblicazione-sottoscrizione si allinea con i principi della DDD che richiedono questi due sistemi non essere a conoscenza della vicenda e, quindi, non parlare direttamente uno a altro. Così, invece, io uso un concetto chiamato uno strato anti-corruzione. Ogni sistema comunicherà attraverso l'operatore che mescolerà i messaggi tra i due.

Questo operatore è una coda di messaggi. Sistema A invia messaggi alla coda. Sistema B consente di recuperare i messaggi dalla coda. Nel mio esempio, io sono solo un abbonato — sistema B — ma è possibile avere molti abbonati.

Qual è nel caso il messaggio?

Quando l'evento viene pubblicato è l'evento CustomerCreated, sistema A volontà di inviare un messaggio che dice: "un cliente è stato inserito. Qui è identità e il nome del cliente." Questo è il messaggio completo tranne che è rappresentato in dati, non in inglese frasi di Nizza. La parte interessante circa la pubblicazione di un evento in una coda di messaggi è che l'editore non si cura quali sistemi recuperano il messaggio o cosa faranno in risposta.

Sistema B risponderà a tale messaggio inserendo o aggiornando il cliente nel suo database. In realtà, B sistema anche non eseguire questo compito; Vi svelo un servizio fare il lavoro. Inoltre, vi svelo il database determinare come un aggiornamento deve essere eseguita. In questo caso, il database di sistema B eseguirà un "aggiornamento" tramite una stored procedure cui logica elimina il record del cliente originale e inserisce una nuova cliente. Perché sarò con GUID come chiavi di identità, l'identità del cliente sarà mantenuta correttamente. Non voglio preoccuparti generati dal database chiavi nel mio software DDD. Vs GUID creato in precedenza. incrementato da database di chiavi è un argomento appiccicoso. Sarà necessario definire la logica per allinearsi con le pratiche del database aziendale.

Alla fine, B del sistema, il sistema di ordinazione, avrà un elenco completo dei clienti che può utilizzare. Ulteriormente lungo nel flusso di lavoro, se il sistema di ordinazione ha bisogno di ulteriori informazioni su un particolare cliente, ad esempio informazioni di carta di credito o indirizzo di spedizione attuale, io posso sfruttare altri meccanismi, come la chiama un servizio, per recuperare i dati se necessario. Non affrontare tale flusso di lavoro qui, comunque.

Comunicando con la coda di messaggi

Un sistema che permette di comunicare messaggi in modo asincrono è chiamato un bus di eventi. Un bus di eventi contiene l'infrastruttura per archiviare i messaggi e li forniscono a chi ha bisogno di recuperarli. Inoltre fornisce un'API per interagire con esso. Mi concentrerò su una particolare implementazione che ho trovato per essere un modo semplice per iniziare con questo stile di comunicazione: una coda di messaggi. Ci sono un certo numero di code di messaggi tra cui scegliere. Nel corso di fondamenti di DDD Pluralsight.com, Smith e ho scelto di utilizzare il SQL Server Service Broker come nostra coda di messaggi. Perché entrambi lavoriamo con SQL Server, e ' stato semplice per noi impostare e dovevamo scrivere SQL push i messaggi alla coda e recuperarli.

Nello scrivere questo articolo, ho deciso che era tempo per me di imparare ad usare uno della più popolari OpenSource le code di messaggi, RabbitMQ. Questo significava che l'installazione del server di RabbitMQ (ed Erlang!) sul mio computer, come pure tirando nel Client .NET RabbitMQ così potuto facilmente codice contro di esso nella mia applicazione. Potete saperne di più sulle RabbitMQ a rabbitmq.com. Ho trovato anche il RabbitMQ per i corsi di sviluppatori .NET su Pluralsight.com di essere molto utile.

Sistema A, pertanto, ha un meccanismo per l'invio di messaggi al server RabbitMQ. Ma il sistema B, il sistema di ordine, non ha nulla a che fare con qualcuno di questa interazione. Sistema B semplicemente si attende la lista dei clienti di essere nel database e non si cura di come ci si arriva. Piccolo servizio Windows separato gestirà controllando la coda RabbitMQ di messaggi per i messaggi e aggiornando il database di sistema di ordine di conseguenza. Figura 1 presenta un flusso di lavoro visiva dell'intero processo.

coda di messaggi consente di condividere i messaggi, in questo caso per aggiornare il Database del sistema B Unacquainted sistemi
Figura 1 coda di messaggi consente di condividere i messaggi, in questo caso per aggiornare il Database del sistema B Unacquainted sistemi

Invio di messaggi alla coda

Inizierò con la classe Customer nel sistema A, mostrato Figura 2. Per amore di un semplice esempio, esso comprende solo alcune proprietà — ID, nome, l'origine di alcune date di registrazione e il cliente. Seguendo modelli DDD, l'oggetto ha vincoli incorporati per impedire la modifica casuale. Si crea un nuovo cliente utilizzando il metodo Create fabbrica. Se avete bisogno di fissare il nome, si utilizza il metodo FixName.

Figura 2 la classe Customer nel contesto delimitata manutenzione cliente

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

Si noti che sia il costruttore che il metodo FixName chiama il metodo PublishEvent, che, a sua volta, crea un semplice CustomerDto (che ha solo una proprietà Id e nome) e poi usi il DomainEvents classe da articolo di MSDN Magazine di Udi Dahan 2009, "Impiegando il modello di dominio Model" (msdn.microsoft.com/magazine/ee236415) per generare un nuovo cliente­UpdatedEvent (vedere Figura 3). Nel mio esempio, sto pubblicando l'evento in risposta alle azioni semplici. In un'implementazione reale, potreste preferire di pubblicare questi eventi dopo i dati sono stato con successo reso persistente nel database di sistema un.

Figura 3 classe che incapsula un evento quando un cliente viene aggiornato

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"; }
 }}

Coprispalle CustomerUpdatedEvent s tutto quello che devi sapere su questo evento: il CustomerDto insieme con un flag che indica se il cliente è nuovo. C'è anche metadati che saranno necessari da un gestore di evento generico.

Il CustomerUpdatedEvent può essere poi gestita da uno o più gestori che definisco nella mia applicazione. Ho definito un solo gestore, un servizio chiamato CustomerUpdatedService:

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

Il servizio consente di gestire tutte le istanze di CustomerUpdatedEvent mio codice genera utilizzando l'editore di messaggio specificato a pubblicare l'evento. Io non ho specificato l'editore qui; Io ho solo fatto riferimento a un'astrazione, IMessagePublisher. Io sto impiegando un modello di cio che mi permette di accoppiare liberamente mia logica. Sono una ragazza volubile. Oggi posso voglio un editore del messaggio. Domani, potrei preferire altro. Sullo sfondo, ho usato StructureMap (structuremap.net), uno strumento popolare tra gli sviluppatori .NET per la gestione di IoC in applicazioni .NET. StructureMap mi consente di indicare dove trovare le classi di gestiscono gli eventi generati da DomainEvents.Raise. Autore di StructureMap, Jeremy Miller, ha scritto una serie eccellente in MSDN Magazine chiamato "Patterns in Practice" che è rilevante per i modelli applicati in questo campione (bit.ly/1ltTgTw). Con StructureMap, ho configurato la mia applicazione di sapere che quando vede IMessagePublisher, deve utilizzare la classe concreta, RabbitMQMessagePublisher, la cui logica è mostrato qui:

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

Nota che ho rimosso un numero di righe di codice specifico di configurazione RabbitMQ. Potete vedere l'elenco completo in download (msdn.microsoft.com/magazine/msdnmag1014).

La carne di questo metodo è che esso pubblica rappresentazione JSON dell'oggetto evento nella coda. Ecco quello che sembra quando ho aggiunto un nuovo cliente denominato Julie Lermantale stringa:

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

Quando questo messaggio è stato pubblicato, il coinvolgimento del sistema cliente manutenzione è completa.

Nell'applicazione di esempio, io uso una serie di test per causare dei messaggi pubblicati in coda, come mostrato Figura 4. Piuttosto che costruire test che verifica la coda quando hanno finito, semplicemente navigare a gestione RabbitMQ sul mio computer e utilizzare i suoi utensili. Notare nel costruttore prova che inizializzare una classe denominata IoC. Questo è dove ho configurato StructureMap per legare i IMessagePublisher e i gestori eventi.

Figura 4 editoria al RabbitMq nei test

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

Recupero del messaggio e aggiornando Database del sistema di ordine

Il messaggio si trova sul server RabbitMQ in attesa di essere estratto. E tale attività viene eseguita da un servizio di Windows che gira continuamente, periodicamente il polling della coda per i nuovi messaggi. Quando vede un messaggio, il servizio recupera e gestisce. Il messaggio può anche essere gestito altri Sottoscrittori come vengono. Per il bene di questo esempio, ho creato una semplice applicazione console, piuttosto che un servizio. Questo mi permette facilmente eseguire ed eseguire il debug il «servizio» da Visual Studio mentre l'apprendimento. Per la prossima iterazione, potrei controllare Microsoft Azure WebJobs (bit.ly/1l3PTYH), piuttosto che aggrovigliare con un Windows service o usando il mio trucco di applicazione console.

Il servizio si avvale di simili modelli di generazione di eventi con classe DomainEvents Dahan, risponde agli eventi in una classe del gestore e l'inizializzazione di una classe di cio che utilizza StructureMap per individuare i gestori eventi.

Ascolta il servizio RabbitMQ per i messaggi utilizzando la classe RabbitMQ .NET sottoscrizione Client. Si può vedere la logica per questo nel metodo scrutinio seguente, dove l'oggetto _subscription mantiene ascolto dei messaggi. Ogni volta che un messaggio viene recuperato, deserializza il JSON memorizzati nella coda in un CustomerUpdatedEvent e quindi genera l'evento:

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

Il servizio contiene una sola classe, cliente:

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

Quando il CustomerUpdatedEvent viene deserializzato, sua proprietà cliente — originariamente popolato da un CustomerDto nel sistema di gestione del cliente — deserializza all'oggetto di questo servizio cliente.

Che cosa accade l'evento generato è la parte più interessante del servizio. Qui è la classe, CustomerUpdatedHandler, che gestisce l'evento:

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

Questo servizio utilizza il Entity Framework (EF) di interagire con il database. Figura 5 dimostra l'interazione rilevante è incapsulata in due metodi — InsertCustomer e UpdateCustomer — in un semplice repository. Se la proprietà IsNew dell'evento è true, il servizio chiama il metodo InsertCustomer del repository. In caso contrario, chiama il metodo UpdateCustomer.

Figura 5 il InsertCustomer e metodi 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);
}}

Quei metodi eseguono la logica pertinente utilizzando un DbContext EF. Un inserimento, aggiunge il cliente e quindi chiama il metodo SaveChanges. EF eseguirà il comando di inserimento del database. Per un aggiornamento, si invierà il CustomerID e CustomerName a una stored procedure, che utilizza qualsiasi logica io — o mio fidato DBA — ha definito per eseguire l'aggiornamento.

Pertanto, il servizio esegue i lavori necessari nel database per assicurarsi che l'elenco dei clienti del sistema di ordinazione ha sempre un roster aggiornato di clienti come sostenuto nel sistema di manutenzione del cliente.

Sì, questo è un sacco di livelli e pezzi del Puzzle!

Perché ho usato un semplice campione di dimostrare questo flusso di lavoro, si potrebbe pensare che la soluzione è una quantità incredibile di overkill. Ma ricordate, il punto è che questo è come orchestrare la soluzione quando si utilizza DDD pratiche per risolvere i problemi di software complesso. Messa a fuoco sul dominio di manutenzione del cliente, quando non interessa altri sistemi. Mediante astrazioni con cio, i gestori e le code di messaggi, possibile per soddisfare le esigenze dei sistemi esterni senza confondere il dominio stesso. La classe cliente semplicemente genera un evento. Per questa demo, questo è il posto più facile per assicurare il flusso di lavoro non ha senso per i lettori, ma potrebbe già essere troppo fangoso per il tuo dominio. Si può sicuramente generare l'evento da un altro posto nell'applicazione, forse da un repository proprio come sta per spingere le modifiche nel proprio archivio dati.

Il download della soluzione campione per questa colonna impiegano RabbitMQ e che richiede l'installazione di un server open source leggero sul tuo computer. Ho incluso riferimenti nel file ReadMe di download. Vi posterò anche un breve video sul mio blog a thedatafarm.com, così si può vedere me scorrendo il codice, ispezionando il Manager RabbitMQ e il database per visualizzare i risultati.


Julie Lerman è un Microsoft MVP, mentore e consulente .NET che risiede nel Vermont. Puoi trovare suo presentando dati accesso e other.NET argomenti a gruppi di utenti e conferenze in tutto il mondo. Blog di lei a /Blog ed è l'autore di "programmazione Entity Framework" (2010), nonché un codice prima edizione (2011) e un DbContext edizione (2012), tutti da o ' Reilly Media. Seguirla su Twitter a Twitter.com /julielerman. e vedere i suoi corsi Pluralsight presso Juliel.me/PS-video.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: Cesar de la Torre