Octubre de 2016

Volumen 31, número 10

Tecnología de vanguardia: enfoque de evento-comando-saga de lógica empresarial

Por Dino Esposito

Dino EspositoSi pudiésemos eliminar requisitos relativamente congelados, cualquier esfuerzo de diseño realizado antes del desarrollo valdría la pena. Si recuerda el término "Big Design Up Front", sabe a qué me refiero. De lo contrario, puede investigarlo en bit.ly/1jVQr3g. Un modelo de dominio completo que trata tanto flujos de trabajo abundantes y complicados como vistas de datos avanzadas y no normalizadas requiere un conocimiento sólido y estable del dominio, además de extender el tiempo de desarrollo. En otras palabras, dada la importancia que el software tiene actualmente para los negocios cotidianos, un modelo de dominio completo es tan malo ahora como el enfoque Big Design Up Front antes de la aparición del movimiento Agile.

El enfoque de evento-comando-saga (ECS) promueve un método mucho más ágil para implementar flujos de trabajo empresariales. Todavía requiere un sólido conocimiento de los procesos y las reglas de negocios, pero no necesita un gran diseño para su implementación antes del inicio de la codificación. Además, es flexible a la hora de acomodar los cambios y, aún más importante, los aspectos empresariales que inicialmente se omitían o subestimaban.

El enfoque ECS promueve una fórmula de procesos empresariales basada en mensajes, que se acerca increíblemente al nivel de abstracción de los diagramas de flujo. Por tanto, es algo que las partes interesadas pueden comunicar y validar. Una fórmula de procesos empresariales basada en mensajes también es mucho más fácil de comprender para los desarrolladores, incluso cuando su conocimiento del dominio empresarial específico es limitado. Probablemente, el término ECS sonará nuevo, pero los conceptos en los que se basa son los mismos que puede encontrar bajo la denominación CQRS/ES en otros orígenes.

En la columna de este mes, presentaré un marco basado en .NET concebido específicamente para implementar la lógica empresarial de las aplicaciones con conceptos relativamente nuevos, tales como comandos y sagas. Para obtener una introducción al tema, puede consultar la columna de septiembre de 2016 (msdn.com/magazine/mt767692). Aquí, el término "lógica empresarial" abarca la aplicación y la lógica del dominio. La "lógica de la aplicación" es donde se implementan todos los casos de uso que dependen de un front-end determinado. En cambio, la "lógica del dominio" es invariable para los casos de uso y completamente reutilizable en todos los tipos de presentación y niveles de aplicación que pueda tener.

MementoFX en acción

Vamos a empezar con un nuevo proyecto de ASP.NET MVC ya configurado para usar elementos comunes, como Bootstrap, jQuery, Entity Framework y ASP.NET SignalR. Luego, agregamos una clase de controlador con un método y una vista relacionada que muestre a los usuarios un formulario HTML. Cuando el usuario envíe el formulario, se prevé que se ejecute el siguiente código:

[HttpPost]
public ActionResult Apply(NewAccountRequestViewModel input)
{
  _service.ApplyRequestForNewBankAccount(input);
  return RedirectToAction("index", "home");
}

A primera vista, se trata de código estándar, pero la diversión está justo debajo de la superficie. Abra el código del método ApplyRequestForNewBankAccount en el nivel de aplicación.

Con fines empresariales, el usuario de la aplicación (probablemente, un empleado de banco) acaba de rellenar el formulario con la solicitud de un cliente para abrir una nueva cuenta. Existe un proceso específico que se debe iniciar siempre que se recibe una nueva solicitud. Puede programar de manera procedimental todos los pasos del flujo de trabajo directamente en el nivel de aplicación, o bien puede probar el enfoque ECS. En el último caso, esto es lo que puede conseguir:

public void ApplyRequestForNewBankAccount(NewAccountRequestViewModel input)
{
  var command = new RequestNewBankAccountCommand(
    input.FullName, input.Age, input.IsNew);
  Bus.Send(command);
}

La clase RequestNewBankAccountCommand es algo más que una clase de objeto CLR estándar (POCO). Es una clase POCO, pero se hereda de Command. A su vez, la clase Command se define en uno de los paquetes NuGet que forman el marco MementoFX. Luego, se agregan los paquetes como se muestra en la Figura 1.

Instalación de paquetes NuGet MementoFX
Figura 1 Instalación de paquetes NuGet MementoFX

El marco MementoFX está formado por tres componentes principales: una biblioteca principal, un almacén de eventos y un bus. En la configuración de muestra, usé la versión incrustada de RavenDB para almacenar eventos de dominio y un bus en memoria (Postie) para la capa de aplicación y sagas para publicar eventos y suscribirse a estos. Si examina la plataforma NuGet de manera más profunda, también encontrará un componente de bus basado en Rebus y un componente de almacén de eventos basado en MongoDB. De este modo, el siguiente código se compila ahora sin problemas:

public class RequestNewBankAccountCommand : Command
{
  ...
}

En la versión actual de MementoFX, la clase base Command es un simple marcador y no contiene código funcional, algo que probablemente cambiará en versiones futuras. El comando encapsula los parámetros de entrada del primer paso del proceso empresarial. Para desencadenar el proceso empresarial, el comando se coloca en el bus.

Configuración del entorno de MementoFX

La configuración inicial de MementoFX es más fácil si se usa un marco de Inversión de control (IoC), como Unity. Para configurar MementoFX, debe realizar las tres acciones siguientes: Primero, inicialice el almacén de eventos de su elección. Segundo, indique a MementoFX cómo resolver tipos de interfaz genéricos en tipos concretos (principalmente, se trata de indicar al marco el tipo de bus que debe usar, así como el almacén de eventos y el documento del almacén de eventos). Tercero, resuelva el bus en una instancia concreta y enlácelo a sagas y controladores según corresponda. La Figura 2 resume el proceso.

Figura 2 Configuración de MementoFX

// Initialize the event store (RAVENDB)
NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(8080);
var documentStore = new EmbeddableDocumentStore
{
  ConnectionStringName = "EventStore",
  UseEmbeddedHttpServer = true
};
documentStore.Configuration.Port = 8080;
documentStore.Initialize();
// Configure the FX
var container = MementoFxStartup
  UnityConfig<InMemoryBus, EmbeddedRavenDbEventStore,
     EmbeddableDocumentStore>(documentStore);
// Save global references to the FX core elements
Bus = container.Resolve<IBus>();
AggregateRepository = container.Resolve<IRepository>();
// Add sagas and handlers to the bus
Bus.RegisterSaga<AccountRequestSaga>();
Bus.RegisterHandler<AccountRequestDenormalizer>();

Como se muestra en la Figura 2, el bus cuenta con dos suscriptores: AccountRequestSaga obligatorio y AccountRequestDenormalizer opcional. La saga contiene el código que lleva a cabo el trabajo de procesar la solicitud. Toda la lógica empresarial que pueda tener se aplica aquí. El desnormalizador recibirá la información sobre el agregado y, si es necesario, creará una proyección de los datos exclusiva para fines de consulta.

Diseño de la saga

Una saga es una clase que representa una instancia en ejecución de un proceso empresarial. En función de las funcionalidades reales del bus que use, la saga se podrá conservar, suspender y reanudar según sea necesario. El bus predeterminado que tiene en MementoFX solo funciona en la memoria. Así, cualquier saga es un proceso extraordinario que se ejecuta de forma transaccional de principio a fin.

Una saga debe tener un evento de inicio o comando. El mensaje de inicio se indica a través de la interfaz IAmStartedBy. Cualquier mensaje adicional (comando o evento) que la saga sepa cómo controlar se enlaza a través de la interfaz IHandlesMessage:

public class AccountRequestSaga : Saga,
  IAmStartedBy<RequestNewBankAccountCommand>,
  IHandleMessages<BankAccountApprovedEvent>
{
  ...
}

Ambas interfaces están formadas por un solo método Handle, como se muestra a continuación:

public void Handle(RequestNewBankAccountCommand message) { ... }
public void Handle(BankAccountApprovedEvent message) { ... }

Volvamos al formulario HTML que se supone que tenía en la interfaz de usuario. Cuando el empleado del banco hace clic para enviar la solicitud del cliente para abrir una nueva cuenta bancaria, se inserta un comando en el bus y este desencadena de forma silenciosa una nueva saga. Finalmente, se ejecuta el método Handle de la saga para el comando especificado.

Adición de comportamiento a la saga

La instancia de una clase de saga se crea de la siguiente manera:

public AccountRequestSaga(
  IBus bus, IEventStore eventStore, IRepository repository)
  : base(bus, eventStore, repository)
{
}

Obtiene una referencia al bus para que la saga actual pueda insertar nuevos comandos y eventos en el bus para otras sagas u otros controladores y desnormalizadores que se vayan a procesar. En realidad, este es el factor clave que permite un diseño ágil y flexible de los flujos de trabajo empresariales. Además, una saga obtiene una referencia al repositorio. En MementoFX, el repositorio es una fachada construida sobre el almacén de eventos. El repositorio guarda y devuelve agregados, a excepción del estado del agregado, que se vuelve a generar cada vez mediante la reproducción de todos los eventos por los que pasó. De manera bastante satisfactoria, el repositorio de MementoFX también ofrece una sobrecarga para consultar el estado de un agregado determinado en una fecha determinada.

A continuación, se muestra una saga que conservaría la solicitud de una nueva cuenta bancaria:

public void Handle(RequestNewBankAccountCommand message)
{
  var request = AccountRequest.Factory.NewRequestFrom(
    message.FullName, message.Age, message.IsNew);
  Repository.Save(request);
}

En este ejemplo, la clase AccountRequest es un agregado de MementoFX. Un agregado de MementoFX es una clase estándar derivada de una clase primaria específica. La asignación de una clase primaria le ahorrará el trabajo de programar un montón de elementos en relación con la administración de eventos de dominio internos:

public class AccountRequest : Aggregate,
  IApplyEvent<AccountRequestReceivedEvent> { ... }

Otro aspecto interesante de los agregados de MementoFX es la interfaz IApplyEvent. El tipo asociado a la interfaz IApplyEvent define un evento de dominio adecuado para el agregado del que se va a realizar el seguimiento. En otras palabras, significa que todos los eventos asociados a la interfaz IApplyEvent se guardan en el almacén de eventos de esa instancia de la clase de agregado. Por lo tanto, de una solicitud de cuenta bancaria, puede saber cuándo de produjo su recepción, procesamiento, aprobación, retraso, denegación, etc. Además, todos los eventos se almacenarán en su orden natural, lo que facilitará al marco la selección de todos los eventos hasta una fecha determinada y la devolución de una vista del agregado en cualquier momento de la vida útil del sistema. Tenga en cuenta que, en MementoFX, el uso de la interfaz IApplyEvent es opcional, en el sentido en que también se recomienda guardar manualmente los eventos importantes en el almacén cuando se invoca algún otro método del agregado. El uso de la interfaz es una práctica recomendada que mantiene el código más claro y conciso.

Al definir un agregado, se debe indicar su id. único. Por convención, MementoFX reconoce como id. una propiedad con el nombre de la clase de agregado, más "Id". En este caso, habría sido AccountRequestId. Si quiere usar otro nombre (por ejemplo, RequestId), debe usar el atributo AggregateId, como se muestra a continuación:

public void ApplyEvent(
  [AggregateId("RequestId")]
  AccountRequestReceivedEvent theEvent)
{ ... }

En C# 6, también puede usar el operador nameof para evitar usar una constante estándar en el código compilado. Con MementoFX y el enfoque ECS, debe modificar un poco la lógica de persistencia que está acostumbrado a usar. Por ejemplo, cuando la saga está a punto de registrar la solicitud de la cuenta, usa el generador de AccountRequest para obtener una nueva instancia. Tenga en cuenta que, para evitar errores en tiempo de compilación, la clase del generador debe estar definida en el cuerpo de la clase AccountRequest:

public static class Factory
{
  public static AccountRequest NewRequestFrom(string name, int age, bool isNew)
  {
    var received = new AccountRequestReceivedEvent(Guid.NewGuid(), name, age, isNew);
    var request = new AccountRequest();
    request.RaiseEvent(received);
    return request;
  }
}

Como puede ver, el generador no rellena la instancia recién creada del agregado, sino que solo prepara un evento y lo genera. El método RaiseEvent pertenece a la clase base Aggregate, tiene el efecto de agregar ese evento a la instancia actual del agregado y llama a ApplyEvent. Así, de manera aparentemente completa, está en el punto de devolver del generador un agregado completamente inicializado. No obstante, la ventaja no es solo que el agregado contiene el estado actual, sino que también contiene todos los eventos importantes introducidos en la operación actual.

¿Qué sucede cuando la saga guarda el agregado en la capa de persistencia? El método Save del repositorio integrado recorre la lista de eventos pendientes del agregado y los escribe en el almacén de eventos configurado. Cuando se llama al método GetById en su lugar, este toma el id. para recuperar todos los eventos relacionados y devuelve una instancia del agregado, que resulta de la reproducción de todos los eventos registrados. En la Figura 3 se muestra una interfaz de usuario muy parecida a la que puede imaginar con un enfoque estándar. No obstante, lo que sucede en el interior es bastante diferente. Tenga en cuenta que en la interfaz de usuario usé ASP.NET SignalR para devolver los cambios a la página principal.

Aplicación MementoFX de ejemplo en acción
Figura 3 Aplicación MementoFX de ejemplo en acción

Un apunte sobre los desnormalizadores

Uno de los cambios más importantes introducidos en el software recientemente es la separación entre el modelo ideal para guardar datos y el modelo ideal para consumirlos, según el patrón CQRS. Hasta ahora, solo guardaba un agregado con toda la información que era importante guardar. No obstante, cada tipo de usuario podía tener un conjunto de información importante diferente del mismo agregado. Cuando esto sucede, se deben crear una o más proyecciones de datos almacenados. Una proyección en este contexto se parece mucho a una vista en una tabla de SQL Server. Los desnormalizadores se pueden usar para crear proyecciones de agregados. Un desnormalizador es un controlador enlazado a un evento insertado en el bus. Por ejemplo, imagine que necesita crear un panel para los administradores responsables de aprobar las nuevas solicitudes de cuenta. Es posible que quiera ofrecer una agregación ligeramente distinta de los mismos datos, quizás con algunos indicadores importantes para la empresa:

public class AccountRequestDenormalizer :
  IHandleMessages<AccountRequestReceivedEvent>
  {
    public void Handle(AccountRequestReceivedEvent message)
    { ... }
}

No es necesario guardar los datos desnormalizados en el almacén de eventos. Puede usar de manera razonable cualquier base de datos que elija para este fin; casi siempre, un motor relacional clásico es la solución más eficaz.

Resumen

Esta columna ofrecía un esbozo de un nuevo método para organizar la lógica empresarial, en el que se unen CQRS y Event Sourcing, pero sin tratar con los detalles y complejidades de bajo nivel de ambos patrones. Además, el enfoque ECS también se acerca a la empresa real para favorecer la comunicación y reducir el riesgo de malentendidos. MementoFX está en NuGet para que pueda probarlo. Estoy impaciente por recibir sus comentarios.


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 mundiales de la industria, 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