Julio de 2015

Volumen 30, número 7

Tecnología de vanguardia: CQRS y las aplicaciones basadas en mensajes

Por Dino Esposito | Julio de 2015

Dino EspositoAl final del día, la segregación de responsabilidad de consultas y comandos (CQRS) es un diseño de software que separa el código que modifica el estado del código que simplemente lee el estado. Esa separación puede ser lógica y basarse en distintas capas. También puede ser física e implicar distintos niveles. No hay ningún manifiesto o filosofía de moda detrás de CQRS. El único impulsor es su sencillez de diseño. Un diseño simplificado en estos días locos de gran complejidad empresarial es la única manera segura para garantizar el éxito, la optimización y la eficacia.

En mi última columna (msdn.microsoft.com/magazine/mt147237) ofrecí una perspectiva del método CQRS que permitía su uso para cualquier tipo de aplicación. El momento en que consideren la posibilidad de usar una arquitectura CQRS con distintas pilas de comandos y consultas, empiecen a pensar en maneras de optimizar cada pila por separado.

Ya no existen restricciones de modelo que hacen que determinadas operaciones presenten un riesgo, sean poco prácticas o quizás demasiado costosas. La visión del sistema se vuelve mucho más orientado a las tareas. Lo que es más importante, tiene lugar como un proceso natural. Incluso algunos conceptos de diseño impulsados por el dominio, como agregados, dejan de tener un aspecto tan molesto. Incluso ellos encuentran su lugar natural en el diseño. Se trata del poder de un diseño simplificado.

Si tienen suficiente curiosidad sobre CQRS como para empezar a buscar casos prácticos y aplicaciones que se aplican a su negocio, encontrarán que la mayoría de las referencias hacen referencia a los escenarios de aplicaciones que usan eventos y mensajes para diseñar e implementar lógica de negocios. Si bien CQRS puede funcionar con aplicaciones mucho más sencillas, por ejemplo, las que de otro modo se podrían considerar como aplicaciones CRUD simples, no cabe duda de que destaca en situaciones con mayor complejidad de negocios. A partir de ahí, pueden deducir mayores complejidades de reglas de negocios y alta inclinación al cambio.

Arquitectura basada en mensajes

Mientras observan el mundo real, verán las acciones en proceso y los eventos que resultan de esas acciones. Las acciones y los eventos transportan datos y, a veces, generan datos nuevos... y eso es el objetivo. Son solo datos. No necesariamente se requiere un modelo de objetos completo para admitir la ejecución de estas acciones. Un modelo de objetos todavía puede resultar útil. Sin embargo, como verán en breve, es simplemente otra posible opción para organizar la lógica de negocios.

Una arquitectura basada en mensajes es beneficiosa porque simplifica significativamente la administración de flujos de trabajo empresariales complejos, elaborados y de rápida evolución. Entre estos tipos de flujos de trabajo se incluyen las dependencias en código heredado, servicios externos y reglas que cambian de manera dinámica. Sin embargo, la creación de una arquitectura basada en mensajes sería casi imposible fuera del contexto de CQRS, el cual mantiene las pilas de comando y consultas separadas de manera ordenada. Por lo tanto, pueden usar la siguiente arquitectura para la pila singular de comandos.

Un mensaje puede ser un comando o un evento. En el código, normalmente se define una clase base Message y a partir de ahí, se definen clases base adicionales para los comandos y eventos, como se muestra en la figura 1.

Figura 1. Definición de la clase base Message

public class Message
{
  public DateTime TimeStamp { get; proteted set; }
  public string SagaId { get; protected set; }
}
public class Command : Message
{
  public string Name { get; protected set; }
}
public class Event : Message
{
  // Any properties that may help retrieving
  // and persisting events.
}

Desde el punto de vista semántico, los comandos y eventos son entidades ligeramente diferentes y sirven para propósitos diferentes pero relacionados. Un evento es casi igual que en Microsoft .NET Framework: una clase que transporta datos y les notifica cuando algo ha sucedido. Un comando es una acción que se lleva a cabo en el back-end y que un usuario u otro componente del sistema ha solicitado. Los eventos y comandos siguen convenciones de nomenclatura estándar. Los comandos se redactan en formato imperativo, como por ejemplo SubmitOrderCommand, mientras que los eventos se forman usando el tiempo verbal pasado, como OrderCreated.

Normalmente, al hacer clic en cualquier elemento de la interfaz se origina un comando. Una vez que el sistema reciba el comando, se origina una tarea. La tarea puede ser cualquier cosa, desde un proceso de larga ejecución con estado, una sola acción o un flujo de trabajo sin estado. Un nombre común para esta tarea es "saga".

Una tarea es unidireccional; avanza a partir de la presentación hacia abajo, pasando por el software intermedio, y suele terminar modificando el estado del sistema y de almacenamiento. Los comandos no suelen devolver datos a la presentación, excepto quizás alguna forma rápida de comentarios, por ejemplo, si la operación se completó correctamente o los motivos por los que se produjo un error.

Las acciones de usuario explícitas no son la única manera de desencadenar los comandos. También pueden colocar un comando con servicios autónomos que interactúan de manera asincrónica con el sistema. Piensen en un escenario de B2B, como el envío de productos, en el que la comunicación entre los socios se produce a través de un servicio HTTP.

Eventos en una arquitectura basada en mensajes

Los comandos originan tareas y las tareas a menudo constan de varios pasos que se combinan para formar un flujo de trabajo. Con frecuencia, cuando se ejecuta un paso determinado, una notificación de resultados debe pasar a otros componentes para que se produzca un trabajo adicional. La cadena de subtareas que desencadena un comando puede ser larga y compleja. Una arquitectura basada en mensajes es útil porque permite modelar flujos de trabajo desde el punto de vista de eventos y acciones individuales (desencadenadas por los comandos). Al definir componentes de controlador para los comandos y eventos subsiguientes, pueden modelar cualquier proceso empresarial complejo.

Y lo que es más importante, pueden seguir una metáfora de trabajo parecida a la de un diagrama de flujo clásico. Esto simplifica significativamente la comprensión de las reglas y optimiza la comunicación con expertos de dominio. Además, el flujo de trabajo resultante se divide en innumerables controladores más pequeños, cada uno de los cuales lleva a cabo un pequeño paso. Además, cada paso coloca comandos asincrónicos y notifica los eventos a otros agentes de escucha.

Una ventaja importante de este método es que la lógica de aplicación se puede modificar y ampliar fácilmente. Todo lo que hay que hacer es escribir nuevos elementos y agregarlos al sistema. Pueden hacerlo con la total certeza de que no afectarán al código existente y ni a los flujos de trabajo existentes. Para ver por qué este es el caso y cómo funciona realmente, analizaré algunos de los detalles de implementación de la arquitectura basada en mensajes, incluido un nuevo elemento de infraestructura, el bus.

Este es el bus

Para empezar, echaré un vistazo a un componente de bus hecho a mano. La interfaz básica de un bus se resume a continuación:

public interface IBus
{
  void Send<T>(T command) where T : Command;
  void RaiseEvent<T>(T theEvent) where T : Event;
  void RegisterSaga<T>() where T : Saga;
  void RegisterHandler<T>();
}

Normalmente, el bus es un singleton. Recibe las solicitudes para ejecutar comandos y las notificaciones de evento. En realidad, el bus no hace ningún trabajo concreto. Simplemente selecciona un componente registrado para procesar el comando o controlar el evento. El bus contiene una lista de procesos de negocios conocidos desencadenados por eventos y comandos, o avanzados mediante comandos adicionales.

Los procesos que controlan los comandos y eventos relacionados normalmente se denominan sagas. Durante la configuración inicial del bus, se registran los componentes de controlador y saga. Un controlador es simplemente un tipo más sencillo de saga y representa una operación de uso único. Cuando se solicita esta operación, esta se inicia y finaliza sin encadenarse a otros eventos o sin insertar otros comandos en el bus. En la figura 2 se presenta una posible implementación de clase de bus que contiene sagas y controladores en la memoria.

Figura 2 Ejemplo de una implementación de la clase de bus

public class InMemoryBus : IBus
{
  private static IDictionary<Type, Type> RegisteredSagas =
    new Dictionary<Type, Type>();
  private static IList<Type> RegisteredHandlers =
    new List<Type>();
  private static IDictionary<string, Saga> RunningSagas =
    new Dictionary<string, Saga>();
  void IBus.RegisterSaga<T>() 
  {
    var sagaType = typeof(T);
    var messageType = sagaType.GetInterfaces()
      .First(i => i.Name.StartsWith(typeof(IStartWith<>).Name))
      .GenericTypeArguments
      .First();
    RegisteredSagas.Add(messageType, sagaType);
  }
  void IBus.Send<T>(T message)
  {
    SendInternal(message);
  }
  void IBus.RegisterHandler<T>()
  {
    RegisteredHandlers.Add(typeof(T));
  }
  void IBus.RaiseEvent<T>(T theEvent) 
  {
    EventStore.Save(theEvent);
    SendInternal(theEvent);
  }
  void SendInternal<T>(T message) where T : Message
  {
    // Step 1: Launch sagas that start with given message
    // Step 2: Deliver message to all already running sagas that
    // match the ID (message contains a saga ID)
    // Step 3: Deliver message to registered handlers
  }
}

Cuando se envía un comando al bus, pasa por un proceso de tres pasos. En primer lugar, el bus comprueba la lista de sagas registradas para ver si hay sagas registradas configuradas que se deben iniciar tras la recepción de dicho mensaje. Si es así, se crea una instancia de un nuevo componente saga, se pasa el mensaje y este se agrega a la lista de sagas en ejecución. Por último, el bus comprueba si hay algún controlador registrado interesado en el mensaje.

Un evento que se pasa al bus se trata como un comando y se enruta a los agentes de escucha registrados. Sin embargo, si es relevante para el escenario empresarial, puede registrar un evento en algún almacén de eventos. Un almacén de eventos es un almacén de datos sin formato de solo anexo que realiza el seguimiento de todos los eventos de un sistema. El uso de los eventos registrados varía significativamente. Los eventos se pueden solo con fines de seguimiento o usarlos como el único origen de datos (abastecimiento de eventos). Incluso se pueden usar para registrar el historial de una entidad de datos mientras se siguen usando bases de datos clásicas para guardar el último estado de entidad conocido.

Escribir un componente saga

Una saga es un componente que declara la siguiente información: un comando o un evento que inicia el proceso empresarial asociado con la saga, la lista de comandos que la saga puede controlar y la lista de eventos en los que la saga está interesada. Una clase de saga implementa interfaces a través de las cuales declara los comandos y eventos de interés. Las interfaces, como IStartWith y ICanHandle, se definen del modo siguiente:

public interface IStartWith<T> where T : Message
{
  void Handle(T message);
}
public interface ICanHandle<T> where T : Message
{
  void Handle(T message);
}

Este es un ejemplo de la firma de una clase de saga de ejemplo:

public class CheckoutSaga : Saga<CheckoutSagaData>,
       IStartWith<StartCheckoutCommand>,
       ICanHandle<PaymentCompletedEvent>,
       ICanHandle<PaymentDeniedEvent>,
       ICanHandle<DeliveryRequestRefusedEvent>,
       ICanHandle<DeliveryRequestApprovedEvent>
{
  ...
}

En este caso, la saga representa el proceso de finalización de compra de una tienda en línea. La saga comienza cuando el usuario hace clic en el botón checkout y el nivel de aplicación inserta el comando Checkout en el bus. El constructor de la saga genera un identificador único, que se necesita para controlar las instancias simultáneas del mismo proceso empresarial. Deberían poder controlar varias sagas de finalización de la compra de ejecución simultánea. El identificador puede ser un GUID, un valor único que se envía con la solicitud de comando o incluso el identificador de sesión.

Para una saga, el control de un comando o evento incluye tener el método Handle en las interfaces ICanHandle o IStartWith que se invocan desde el componente de bus. En el método Handle, la saga realiza un cálculo o acceso a datos. A continuación, envía otro comando para otras sagas de escucha o simplemente desencadena un evento como una notificación. Por ejemplo, imaginen que el flujo de trabajo de finalización de la compra es como el que se muestra en la figura 3.

Flujo de trabajo de finalización de la compra
Figura 3. Flujo de trabajo de finalización de la compra

La saga realiza todos los pasos hasta la aceptación del pago. En ese momento, inserta un comando AcceptPayment en el bus para que el objeto PaymentSaga pueda continuar. El objeto PaymentSaga se ejecutará y desencadenará un evento PaymentCompleted o PaymentDenied. Estos eventos los controlará nuevamente el objeto CheckoutSaga. A continuación, esa saga avanzará hasta el paso de entrega con otro comando que se inserta en otra saga que interactúa con el subsistema externo de la empresa de transporte asociada.

La concatenación de los comandos y eventos mantiene la saga activa hasta su finalización. En ese aspecto, una saga se podría considerar como un flujo de trabajo clásico con puntos inicial y final. Otra cosa que hay que tener en cuenta es que una saga suele ser persistente. La persistencia normalmente la controla el bus. La clase de bus de ejemplo que se presenta aquí no admite la persistencia. Un bus comercial, como NServiceBus, o incluso un bus de código abierto, como Rebus, podría usar SQL Server. Para que se produzca persistencia, hay que asignar un identificador único para cada instancia de la saga.

Resumen

Para que las aplicaciones modernas sean realmente eficaces, deben ser capaces de escalar con los requisitos empresariales. Una arquitectura basada en mensajes facilita significativamente la ampliación y modificación de los flujos de trabajo empresariales y la compatibilidad con nuevos escenarios. Las extensiones se pueden administrar de manera totalmente aislada. Lo único que hay que hacer es agregar una saga o controlador nuevo, registrarlo en el bus al iniciarse la aplicación e indicarle cómo administrar solo los mensajes que tiene que controlar. El nuevo componente se invocará automáticamente solo cuando llegue el momento y funcionará junto con el resto del sistema. Es fácil, sencillo y eficaz.


Dino Esposito es coautor de "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014) y "Programming ASP.NET MVC 5" (Microsoft Press, 2014). Esposito, evangelizador técnico para las plataformas Android y Microsoft .NET Framework en JetBrains y orador frecuente en eventos del sector en todo el mundo, comparte su visión de software en software2cents.wordpress.com y en Twitter en twitter.com/despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Jon Arne Saeteras