Puntos de datos

Un patrón para compartir datos entre contextos limitados de diseño guiado por el dominio, 2ª parte

Julie Lerman

Descargar el código de muestra

Julie LermanEn el artículo sobre los Puntos de datos (msdn.microsoft.com/magazine/dn802601) que publiqué en octubre de 2014, escribí acerca del patrón para reflejar datos de una base de datos en otra cuando se están utilizando varios contextos limitados (bounded contexts o BC en sus siglas inglesas) de diseños guiados por el dominio (Domain-Driven Design o DDD en sus siglas inglesas), con cada BC aislado en su propia base de datos. El escenario que expuse fue uno en el que el BC de administración de clientes permitía a los usuarios administrar los datos de clientes además de insertar, actualizar y borrar los detalles de los clientes. El segundo BC está dedicado a un sistema de pedidos que necesita acceder a dos elementos críticos de información del cliente: el identificador clave del cliente y el nombre. Debido a que esos sistemas se encuentran en dos BC diferentes, no puede ir de uno a otro para compartir los datos.

El DDD se utiliza para resolver problemas complejos pero, si simplificamos esos problemas en el dominio, esto con frecuencia nos supondrá extrapolar esa complejidad fuera del mismo. Por ello, no es necesario que el BC de gestión de clientes esté pendiente de este intercambio posterior de datos. En mi artículo anterior utilicé el patrón de publicación o suscripción para resolver el problema haciendo uso de Eventos de dominio, una cola de mensajes (RabbitMQ) y un servicio. Había muchas partes móviles.

Intencionadamente, tomé un atajo para evitar sobrecargar la explicación con demasiados conceptos y fue desencadenar las series de eventos directamente desde la clase de cliente, publicando el evento en una cola de mensajes.

Este mes quiero mejorar la solución de dos maneras. Lo que quiero hacer primero es publicar el mensaje desde una zona más lógica del flujo de trabajo: lo haré después de confirmar que el cliente (ya sea nuevo o modificado) se ha guardado correctamente en la base de datos de clientes del sistema. También quiero asegurarme de que el evento se publica solo como respuesta a eventos relevantes. Tiene sentido publicar el evento después de crear un nuevo cliente. Pero, además, quiero abordar la condición hipotética en la que podría necesitar ser más selectiva cuando se publican las actualizaciones en la cola de mensajes. En este caso, me conviene asegurarme de que el mensaje relevante se publica solamente cuando el nombre del cliente cambia. Así, si se modifican otros datos que no tienen nada que ver con el nombre del cliente, el mensaje no se publicará.

Estos dos cambios harán que la solución sea más aplicable a escenarios del mundo real.

Cuando un cliente creado o actualizado se convierte en un cliente guardado

La presente solución genera una notificación cuando se crea un cliente o cuando se modifica su nombre. Tanto el constructor como el método FixName llaman a PublishEvent:

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

PublishEvent desencadena el flujo de trabajo que aparece en el mensaje que se publicará en la cola:

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

Si necesita más detalles acerca de esta solución puede echarle un vistazo al artículo de octubre. En vez de generar los eventos desde la clase, lo que quiero hacer es generarlos después de saber que tanto la instancia del nuevo cliente como la corrección del nombre del cliente se han guardado correctamente en la base de datos.

Esto supone quitar el método PublishEvent y las llamadas realizadas a él desde la clase Customer.

Mi nivel de datos tiene una clase que contiene la lógica de acceso a datos para el agregado de cliente. He movido el método PublishEvent a esta clase y le he cambiado el nombre a PublishCustomerPersistedEvent. En los métodos que guardan clientes en la base de datos, llamo al nuevo evento después de que se completa SaveChanges (vea la ilustración 1).

Ilustración 1. La clase Persistence genera eventos una vez se guardan los datos

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

Al hacer esto, necesitaré también mover la infraestructura para publicar mensajes al proyecto de nivel de datos. La ilustración 2 muestra los proyectos relevantes (Customer­Management.Core y Customer­Management.Infastructure) que creé en el artículo anterior, además de aquellos proyectos creados al mover este evento al nivel de datos. Ahora mismo, CustomerUpdatedEvent, DTO y Service están en el proyecto Infrastructure. Mover la lógica de infraestructura fuera del dominio fue algo satisfactorio, ya que tener que necesitar el código en el dominio principal me importunaba sobremanera.

Estructura del proyecto antes y después de mover la publicación de eventos al nivel de datos
Ilustración 2. Estructura del proyecto antes y después de mover la publicación de eventos al nivel de datos

Para verificar que tanto las inserciones como las actualizaciones publican los mensajes correctos en la cola, realizo dos pruebas. Una tercera prueba se encarga de verificar que una actualización con error no intente publicar mensajes en la cola. Puede ver estas pruebas en la solución de descarga que viene en el artículo.

El el Nivel de datos, no en el Modelo de dominio

Este es un cambio realmente simple, pero me dio sus quebraderos de cabeza (no en el ámbito técnico) al intentar justificar el movimiento del evento fuera del BC para llevarlo al nivel de datos. Incluso tuve un debate acerca del tema mientras paseaba al perro. (No es raro que me ponga a hablar conmigo misma mientras me doy una vuelta por la calle o el bosque. Afortunadamente, como vivo en un lugar tranquilo, no hay nadie que pueda cuestionar mi cordura). El debate terminó de la siguiente manera: Publicar el evento no es algo que deba estar presente en el BC. ¿Por qué no? Pues porque el BC solo se preocupa de sí mismo. Le da igual qué otros BC, servicios o aplicaciones pueda querer o necesitar. Así que decirle al BC "necesito compartir mis datos guardados con el Sistema de pedidos" no sirve de nada. Esto se debe a que no es un evento de dominio, sino un evento relacionado con la persistencia, la cual colocaré en el apartado Evento de aplicación.

Solucionar un nuevo problema generado por el movimiento

Aquí encontrará un problema que se generó cuando moví el evento publicado a mi nivel de persistencia. El método PersistChangeToCustomer se utiliza también para guardar otras ediciones en la entidad Customer. Por ejemplo, la entidad Customer también tiene la capacidad de agregar o actualizar las direcciones de envío y facturación del cliente. Estas direcciones son objetos de valor y crearlas o reemplazarlas con un nuevo conjunto de valores refleja un cambio en el cliente.

Tengo que llamar a PersistChangeToCustomer cuando cualquiera de esas direcciones cambien. Aun así, en este caso no hay necesidad de enviar un mensaje a la cola diciendo que el nombre del cliente ha cambiado.

Así que, ¿cómo hacemos para que el nivel de persistencia sepa que el nombre de cliente no ha cambiado? Una solución instintiva es agregar una propiedad de marca como NameChanged. Pero no quiero tener que estar dependiendo de los booleanos cada vez que necesite hacer un seguimiento del estado detallado. Prefiero generar un evento desde la clase cliente, pero no uno que desencadene otro mensaje en la cola. Y no quiero un mensaje que simplemente diga: "No enviar un mensaje". Entonces, ¿cómo capturo el evento?

Una vez más, llega Jimmy Bogard al rescate con otra solución brillante. En el artículo "A Better Domain Events Pattern" (Un mejor patrón de eventos de dominio) publicado en su blog en mayo de 2014, bit.ly/1vUG3sV), propone coleccionar eventos en vez de generarlos inmediatamente; haciendo esto, permitiremos que el nivel de persistencia tenga acceso a esa colección y maneje los eventos como mejor le venga. El objetivo de este patrón es quitar la clase estática DomainEvents, que no permite controlar cuándo se generan los eventos y que puede conllevar efectos adicionales. Esta línea de pensamiento en cuanto a los eventos de dominio, está a la vanguardia. Mi refactorización evitará eventualmente este problema, pero tengo que admitir que aun estoy atada a la case estática DomainEvents. Como siempre hago, seguiré aprendiendo de estas prácticas y mejorándolas.

Me encanta el planteamiento de Bogard así que, tomando su idea como base, la utilizaré de un modo diferente a como él la utilizó para su implementación. Yo no necesito enviar un mensaje a la cola, solo necesito leer el evento. Y es una forma excelente de capturar este evento en el objeto de cliente sin tener que crear varias marcas de estado aleatorias. Por ejemplo, puedo evitar tener que incluir un booleano que diga: "Se ha corregido el nombre" y establecerlo como verdadero o falso según lo necesite, ya que sería incómodo.

Bogard utiliza una propiedad ICollection<IDomainEvent> denominada propiedad Events en una interfaz IEntity. Si tuviera más de una entidad en mi dominio haría lo mismo o, quizás, la agregaría a una clase base Entity, pero en esta demostración, pondré directamente la nueva propiedad en el objeto Customer. He creado un campo privado y he preparado la colección Events para que sea de solo lectura y que así solo Customer pueda modificarla:

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

A continuación, definiré el evento relevante: CustomerNameFixedEvent, que implementa la interfaz IDomainEvent que utilicé en la primera parte de este artículo. CustomerNameFixedEvent no requiere mucho trabajo. Estableceré la propiedad DateTimeEventOccurred como parte de la interfaz:

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

Ahora, cada vez que llame al método Customer.FixName, podré agregar una instancia de este evento en la colección Events:

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

Esta acción me proporciona algo que está emparejado de una forma más maleable que una propiedad de estado. Es más, puedo agregar una lógica en el futuro a medida que mi dominio vaya evolucionando y sin tener que modificar la estructura de mi clase Customer, además de sacarle todo el provecho en mi método de persistencia.

Ahora el método PersistChangeToCustomer tiene una lógica nueva y revisará este evento en el próximo Customer. Si el evento existe, enviará el mensaje a la cola. La ilustración 3 muestra de nuevo el método al completo con la nueva lógica: la búsqueda del tipo de evento antes de publicarlo.

Ilustración 3. Método PersistChangeToCustomer al revisar el tipo de evento

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

Si la respuesta a SaveChanges ha sido mayor que 0, el método devolverá un booleano que indica si el Customer se ha guardado correctamente. Si se da este caso, el método buscará cualquier CustomerNameFixedEvents en el Customer.Events y publicará, tal y como hizo anteriormente, un mensaje referente al guardado del cliente. Si por alguna razón se produce un error en SaveChanges, esto se le notificará casi con toda probabilidad con una excepción. Sin embargo, también le presto atención al valor de la respuesta para poder devolver un valor "falso" desde el método. La lógica que llama, decidirá lo que hay que hacer en caso de que se produzca un error: o bien intentará guardar de nuevo, o bien enviará una notificación al usuario final o a otro sistema.

Como estoy usando Entity Framework (EF), tenga en cuenta que puede configurar EF6 para volver a probar SaveChanges si se producen errores de conexión transitorios. De todas formas, para cuando reciba una respuesta de SaveChanges, esto ya se habrá llevado a cabo. Puede echarle un vistazo a mi artículo de diciembre de 2013 "Entity Framework 6, Ninja Edition" (msdn.microsoft.com/magazine/dn532202), si desea tener más información sobre DbExecutionStrategy.

He agregado una nueva prueba para comprobar la lógica nueva. Esta prueba crea y guarda un cliente nuevo, edita la propiedad BillingAddress del cliente y guarda el cambio. Mi cola recibe un mensaje notificándole que se ha creado el nuevo Customer, pero no recibe ningún mensaje acerca de la actualización que se encarga de cambiar la dirección:

[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 Bohlen ha revisado este artículo y sugiere probar el patrón "test spy pattern" (que encontrará en inglés en xunitpatterns.com/Test Spy.html) como alternativa para comprobar si los mensajes llegaron a la cola.

Una solución en dos partes

Cómo compartir datos entre contextos limitados es una pregunta que se formulan muchos desarrolladores que están aprendiendo a utilizar DDD. Steve Smith y yo realizamos una aproximación a esta capacidad en nuestro curso Pluralsight "Domain-Driven Design Fundamentals" o Fundamentos del diseño guiado por el dominio (bit.ly/PS-DDD), pero no la demostramos. Me han pedido en múltiples ocasiones más detalles acerca de cómo llevar a cabo esto. En el primer artículo de esta pequeña serie, utilicé una gran variedad de herramientas para construir un flujo de trabajo que permitiera compartir los datos almacenados en una base de datos de BC con otra base de datos que utilizara otro BC diferente. Estructurar un patrón de publicación o suscripción utilizando colas de mensajes, eventos y un contenedor de Inversión de control, me permitió conseguir un patrón emparejado de una forma realmente maleable.

Este mismo mes, amplié la muestra para responder a la siguiente pregunta: Basándonos en el DDD, ¿cuándo es lógico publicar el mensaje? Al principio, desencadené el flujo de trabajo de intercambio de datos publicando el evento desde la clase Customer a medida que este creaba nuevos clientes o nombres de cliente actualizados. Debido a todos los mecanismos que se pusieron en marcha, fue fácil mover en este artículo esa lógica a un repositorio, lo cual me permitió retrasar la publicación del mensaje hasta estar completamente segura de que los datos del cliente se habían guardado correctamente en la base de datos.

Por supuesto, siempre es posible afinar la maquinaria un poco más y seguramente prefiera utilizar herramientas diferentes, pero por ahora debería poder apañarse sin tener que dar más vueltas de tuerca (no pretendía andar usando juegos de palabras, pero ahí queda) para lograr que el patrón DDD permita que los contextos limitados puedan funcionar teniendo bases de datos independientes.


Julie Lerman es una Microsoft MVP, mentora y consultora de .NET que vive en las colinas de Vermont. Puede encontrarla presentando el acceso a datos y otros temas de .NET a grupos de usuarios y en conferencias en todo el mundo. Ella realiza publicaciones en el blog thedatafarm.com/blog y es la autora de "Programming Entity Framework" (2010), así como de una edición de Code First (2011) y una edición de DbContext (2012), todos de O’Reilly Media. Sígala en Twitter en twitter.com/julielerman y vea sus cursos de Pluralsight en juliel.me/PS-Videos.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Stephen Bohlen
Principal Ingeniero de software en el equipo Technical Evangelism and Development (Evangelismo técnico y desarrollo o TED en sus siglas inglesas) de Microsoft Corporation, gracias a sus más de 20 años de experiencia en software y tecnología, Stephen contribuye a que destacados organizaciones asociadas de Microsoft puedan adoptar e implementar estas tecnologías y productos punteros desarrollados por Microsoft.