Diciembre de 2016

Volumen 31, número 13

Tecnología de vanguardia: reescribir un sistema CRUD con eventos y CQRS

Por Dino Esposito

Dino EspositoEl mundo está lleno de sistemas CRUD (Create, Read, Update, Delete) clásicos compilados en torno a una base de datos relacional y completados con fragmentos de lógica empresarial, a veces enterrada en procedimientos almacenados o encerrada en componentes de caja negra. En el núcleo de estas cajas negras se encuentran las cuatro operaciones de CRUD: crear entidades nuevas, leer, actualizar y eliminar. En un nivel de abstracción lo suficientemente alto, eso es todo. Cualquier sistema es, en cierta medida, un sistema CRUD. A veces, las entidades pueden ser bastante complejas y adoptar la forma de agregado.

En el diseño guiado por el dominio (DDD), un agregado es un clúster de entidades con un objeto raíz, relacionado con la empresa. Por este motivo, la creación, actualización o eliminación de una entidad puede estar sujeta a varias reglas empresariales complejas. Incluso leer el estado de un agregado suele ser problemático, principalmente debido a la experiencia del usuario. El modelo que funciona para alterar el estado del sistema no es, necesariamente, el mismo modelo que funciona para presentar datos a los usuarios en todos los casos prácticos.

Llevar el nivel de abstracción de CRUD al máximo conduce a separar lo que altera el estado del sistema de lo que, simplemente, devuelve una vista o más. Esta es la esencia de CQRS (Command and Query Responsibility Segregation), concretamente de la segregación limpia de las responsabilidades de consultas y comandos.

Sin embargo, hay otras cosas que los arquitectos y desarrolladores de software deben tener en cuenta. El estado del sistema se altera en la pila de comandos y, en términos concretos, allí se crean, actualizan y eliminan agregados. Precisamente ese es el punto que hay que replantearse.

Conservar el historial es fundamental para casi todos los sistemas de software. A medida que se escribe software para respaldar a las empresas existentes, aprender del pasado es crucial por dos motivos: para evitar perderse nada de lo que ha pasado y para mejorar los servicios para clientes y empleados.

En la entrega de mayo de 2016 (msdn.com/magazine/mt703431) y junio de 2016 (msdn.com/magazine/mt707524) de esta columna, presenté maneras de ampliar el sistema CRUD clásico a un sistema CRUD histórico. En las columnas de agosto de 2016 (msdn.com/magazine/mt767692) y octubre de 2016 (msdn.com/magazine/mt742866), presenté un patrón evento-comando-saga (ECS) y un marco MementoFX (bit.ly/2dt6PVD) como bases para nuevas maneras de expresar la lógica empresarial que satisface las necesidades diarias.

En esta columna y la siguiente, describiré las dos ventajas mencionadas de conservar el historial de un sistema al reescribir una aplicación de demostración de reserva (la misma que usé en las columnas de mayo y junio) con CQRS y Event Sourcing.

Visión global

Mi aplicación de ejemplo es un sistema de reserva interno para salas de reunión. El caso práctico principal es un usuario registrado que se desplaza por un calendario y reserva uno o más de los espacios de tiempo disponibles de una sala determinada. El sistema administra entidades como Room, RoomConfiguration y Booking y, como puede imaginar, conceptualmente, la aplicación sirve para agregar y editar salas y configuraciones (es decir, cuándo está disponible para la reserva y la duración de los espacios de tiempo individuales), y para agregar, actualizar y cancelar reservas. En la Figura 1 se dejan entrever las acciones que los usuarios del sistema pueden realizar y cómo se estructuran en un sistema CQRS según el patrón ECS.

Acciones de usuario y diseño de alto nivel del sistema
Figura 1 Acciones de usuario y diseño de alto nivel del sistema

Un usuario puede especificar una reserva nueva, moverla, cancelarla e incluso comprobar el estado de la sala para que el sistema sepa que la sala reservada se está usando. El flujo de trabajo subyacente a cada acción se controla en una saga y esta es una clase definida en la pila de comandos. Una clase de saga está formada por métodos de controlador. Cada uno de estos métodos procesa un comando o un evento. Para hacer una reserva (o mover una reserva existente), solo hay que insertar un comando en la pila de comandos. En general, insertar un comando puede ser tan sencillo como invocar directamente el método de la saga correspondiente. También puede pasar por los servicios de un bus.

Para conservar el historial, debe hacer un seguimiento de al menos todos los efectos empresariales de los comandos procesados. En algunos casos, puede que también quiera hacer el seguimiento de los comandos originales. Un comando es un objeto de transferencia de datos que contiene datos de entrada. Un efecto empresarial de ejecutar un comando a través de una saga es un evento. Un evento es un objeto de transferencia de datos que transporta los datos que describen el evento por completo. Los eventos se guardan en un almacén de datos específico. No hay restricciones estrictas sobre la tecnología de almacenamiento que se debe usar para los eventos. Puede ser un sistema de administración de bases de datos relacionales (RDBMS) sencillo o un almacén de datos NoSQL. (Consulte la columna de octubre para obtener información sobre la configuración de MementoFX, RavenDB y el bus).

Coordinación de comandos y consultas

Supongamos que un usuario coloca un comando para reservar un espacio de tiempo en una sala concreta. En un escenario MVC de ASP.NET MVC, el controlador obtiene los datos publicados y coloca un comando para el bus. El bus está configurado para reconocer algunas sagas y cada saga declara los comandos (y/o eventos) que le interesa controlar. Por lo tanto, el bus distribuye el mensaje a la saga. La entrada de la saga son los datos sin procesar que los usuarios especificaron en los formularios de la UI. El controlador de la saga tiene la responsabilidad de convertir los datos recibidos en una instancia de un agregado coherente con la lógica empresarial.

Supongamos que el usuario hace clic para reservar, como se muestra en la Figura 2. El método del controlador que desencadena el botón recibe el id. de la sala, el día, la hora y el nombre de usuario. El controlador de la saga debe convertirlo en un agregado de reserva diseñado específicamente para usar la lógica empresarial esperada. La lógica empresarial se ocupará de cuestiones del área de permisos, prioridades, costos e incluso simple simultaneidad. Sin embargo, como mínimo, el método de la saga debe crear un agregado de reserva y guardarlo.

Reservar una sala de reunión en el sistema de ejemplo
Figura 2 Reservar una sala de reunión en el sistema de ejemplo

A primera vista, el fragmento de código de la Figura 3 no es diferente de un simple sistema CRUD, excepto en su uso de factory y la propiedad Repository pendiente. El efecto combinad de factory y repository en el evento configurado almacena todos los eventos desencadenados en la implementación de la clase Booking.

Figura 3 Estructura de una clase de saga

public class ReservationSaga : Saga,
  IAmStartedBy<MakeReservationCommand>,
  IHandleMessages<ChangeReservationCommand>,
  IHandleMessages<CancelReservationCommand>
{
   ...
  public void Handle(MakeReservationCommand msg)
  {
    var slots = CalculateActualNumberOfSlots(msg);
    var booking = Booking.Factory.New(
      msg.FullName, msg.When, msg.Hour, msg.Mins, slots);
    Repository.Save(booking);
  }
}

Al final, el repositorio no guarda ningún registro con el estado actual de una clase Booking en la que las propiedades están asignadas a las columnas de alguna forma. Solo guarda eventos empresariales en el almacén y, al final, en esta etapa, sabe exactamente qué ha pasado con la reserva (cuándo y cómo se creó), pero no tiene toda la información clásica preparada para mostrarla al usuario. Sabe lo que ha pasado, pero no tiene nada listo para mostrarlo. El código fuente de factory se muestra en la Figura 4.

Figura 4 Código fuente de factory

public static class Factory
{
  public static Booking New(string name, DateTime when,
    int hour, int mins, int length)
  {
    var created = new NewBookingCreatedEvent(
      Guid.NewGuid(), name.Capitalize(), when,
      hour, mins, length);
    // Tell the aggregate to log the "received" event
    var booking = new Booking();
    booking.RaiseEvent(created);
    return booking;
  }
}

No se toca ninguna propiedad de la instancia recién creada de la clase Booking en factory, pero se crea una clase de evento y se rellena con los datos reales que se almacenarán en la instancia, como el nombre en mayúsculas del cliente y el id. exclusivo que hará un seguimiento permanente de la reserva en el sistema. El evento se pasa al método RaiseEvent, parte del marco MementoFX, porque es la clase base de todos los agregados. RaiseEvent agrega el evento a una lista interna que el repositorio revisará al "guardar" la instancia del agregado. Uso el término "guardar" porque eso es exactamente lo que pasa, pero lo escribo entre comillas para enfatizar que es un tipo de acción distinto al del sistema CRUD clásico. El repositorio guarda el evento de creación de reserva con los datos especificados. Concretamente, el repositorio guarda todos los eventos registrados en una instancia del agregado durante la ejecución de un flujo de trabajo empresarial, concretamente un método de controlador de saga, como se muestra en la Figura 5.

Guardar eventos o estados
Figura 5 Guardar eventos o estados

Sin embargo, hacer el seguimiento del evento empresarial resultante de un comando no es suficiente.

Desnormalizar eventos en la pila de consulta

Si observa el sistema CRUD con la idea de conservar el historial de datos, verá que la creación y la lectura de entidades no afecta al historial, pero no se puede decir lo mismo de la actualización y la eliminación. Un almacén de eventos es un elemento de solo anexión, mientras que las actualizaciones y las eliminaciones son eventos nuevos relacionados con los mismos agregados. Sin embargo, una lista de eventos para un agregado determinado lo dice todo del historial, excepto el estado actual. El estado actual es justo lo que necesita presentar a los usuarios.

Aquí es donde entran en juego los desnormalizadores. Un desnormalizador es una clase compilada como colección de controladores de eventos como los que se guardan en el almacén de eventos. Se registra un desnormalizador con el bus y el bus le distribuye eventos cada vez que recibe uno. El efecto inmediato es que un desnormalizador escrito para escuchar el evento creado de una reserva tiene la oportunidad de reaccionar cuando se desencadena una.

Un desnormalizador obtiene los datos del evento y hace lo que necesite, por ejemplo, mantener una base de datos relacional fácil de consultar sincronizada con los eventos registrados. La base de datos relacional (o un almacén NoSQL o caché, si es más fácil de usar o más ventajoso) pertenece a la pila de consulta y su API no obtiene acceso a la lista de eventos almacenada. Además, puede tener varios desnormalizadores para crear vistas ad hoc de los mismos eventos sin procesar. (Profundizaré en este aspecto en mi próxima columna). En la Figura 1, el calendario en el que un usuario selecciona un espacio de tiempo se rellena desde una base de datos relacional sincronizada con los eventos mediante un desnormalizador. En la Figura 6 se muestra el código de la clase de desnormalizador.

Figura 6 Estructura de una clase de desnormalizador

public class BookingDenormalizer :
  IHandleMessages<NewBookingCreatedEvent>,
  IHandleMessages<BookingMovedEvent>,
  IHandleMessages<BookingCanceledEvent>
{
  public void Handle(NewBookingCreatedEvent message)
  {
    var item = new BookingSummary()
    {
      DisplayName = message.FullName,
      BookingId = message.BookingId,
      Day = message.When,
      StartHour = message.Hour,
      StartMins = message.Mins,
      NumberOfSlots = message.Length
    };
    using (var context = new MfxbiDatabase())
    {
      context.BookingSummaries.Add(item);
      context.SaveChanges();
    }  }
  ...
}

En referencia a la Figura 5, los desnormalizadores proporcionan un CRUD solo para fines de lectura. La salida de los desnormalizadores se suele denominar "modelo de lectura". Las entidades del modelo de lectura no suelen coincidir con los agregados que se usan para generar eventos, ya que suelen basarse en las necesidades de la UI.

Actualizaciones y eliminaciones

Supongamos ahora que el usuario quiere mover un espacio de tiempo reservado previamente. Se envía un comando con todos los detalles del nuevo espacio de tiempo y un método de saga se ocupa de escribir un evento desplazado para la reserva. La saga necesita recuperar el agregado y lo necesita en el estado actualizado. Si los desnormalizadores acaban de crear una copia relacional del estado del agregado (y, por lo tanto, el modelo de lectura casi coincide con el modelo de dominio), puede obtener el estado actualizado desde allí. De lo contrario, se crea una copia nueva del agregado y se usa para ejecutar todos los eventos registrados. Al final de esta reproducción, el agregado tiene el estado más actualizado. La reproducción de eventos no es una tarea que se realice directamente. En MementoFX, obtiene un agregado actualizado con una línea de código dentro de un controlador de la saga:

var booking = Repository.GetById<Booking>(message.BookingId);

A continuación, aplica a la instancia la lógica empresarial necesaria. La lógica empresarial genera eventos y estos persisten en el repositorio:

booking.Move(id, day, hour, mins);
Repository.Save(booking);

Si usa el patrón del modelo de dominio y sigue los principios de DDD, el método de movimiento contiene toda la lógica del dominio y los eventos. De lo contrario, ejecute una función con cualquier lógica empresarial y genere eventos directamente en el bus. Al enlazar otro controlador de eventos con el desnormalizador, tiene la oportunidad de actualizar el modelo de lectura.

Para cancelar una reserva, se usa el mismo enfoque. El evento de cancelar una reserva es un evento empresarial y se debe hacer su seguimiento. Esto significa que es recomendable que haya una propiedad booleana en el agregado para realizar la eliminación lógica. Sin embargo, en el modelo de lectura, la eliminación puede ser completamente física, en función de si la aplicación va a consultar las reservas canceladas en el modelo de lectura. Un efecto secundario interesante es que, para volver a generar el modelo de lectura, puede reproducir eventos desde el principio o desde un punto de recuperación. Solo hace falta crear una herramienta ad hoc que use la API del almacén de eventos para leer eventos y llamar a los desnormalizadores directamente.

Usar la API del almacén de eventos

Observe la selección de la lista desplegable de la Figura 2. El usuario quiere ampliar la reserva al máximo desde la hora inicial. La lógica empresarial del agregado debe poder determinarlo y, para ello, debe acceder a la lista de reservas del mismo día posteriores a la hora inicial. Eso no resulta difícil en el sistema CRUD clásico, pero MementoFX también permite consultar eventos:

var createdEvents = EventStore.Find<NewBookingCreatedEvent>(e =>
  e.ToDateTime() >= date).ToList();

El fragmento de código devuelve una lista de eventos NewBookingCreated posteriores a la hora indicada. Sin embargo, no existe ninguna garantía de que la reserva creada siga activa y no se haya movido a otro espacio de tiempo. Es necesario obtener el estado actualizado de los agregados. Puede elegir el algoritmo que quiera. Por ejemplo, puede filtrar en la lista de eventos Created las reservas que ya no estén activas y, a continuación, obtener el id. del resto de reservas. Finalmente, puede comparar el espacio de tiempo real con el que quiere ampliar para evitar solapamientos. En el código fuente de este artículo, codifiqué toda la lógica en un servicio (dominio) independiente de la pila de comandos.

Resumen

El uso de CQRS y de Event Sourcing no está limitado a sistemas concretos con requisitos avanzados de simultaneidad, escalabilidad y rendimiento. Con una infraestructura disponible que le permita trabajar con agregados y flujos de trabajo, cualquier sistema CRUD actual se puede reescribir de forma que aporte muchas ventajas. Estas ventajas son, entre otras:

  • Conservar el historial de datos
  • Una forma más eficaz y resistente de implementar tareas empresariales y cambiar las tareas para reflejar cambios empresariales con un esfuerzo y riesgo de regresión limitado
  • Dado que los eventos son hechos inmutables, son fáciles de copiar y duplicar e incluso se pueden regenerar mediante programación los modelos de lectura que se quieran

Esto significa que el patrón ECS (o CQRS/ES, como se denomina a veces) tiene un enorme potencial de escalabilidad. Además, el marco MementoFX resulta útil aquí porque simplifica las tareas comunes y ofrece una abstracción agregada para facilitar la programación.

MementoFX inserta un enfoque orientado a DDD, pero puede usar el patrón ECS con otros marcos y otros paradigmas, como el paradigma funcional. Además, aún hay otra ventaja que probablemente es la más relevante. Hablaré sobre ella en mi próxima columna.


Dino Esposito es el autor de "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014) y "Modern Web Applications with ASP.NET" (Microsoft Press, 2016). Como experto técnico para las plataformas .NET y Android en JetBrains y orador frecuente en eventos internacionales del sector, Esposito comparte su visión sobre el software en software2cents.wordpress.com y en su Twitter @despos.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Andrea Saltarello