Dezembro de 2016

Volume 31 - Número 13

Cutting Edge - Reescreva um Sistema CRUD com Eventos e CQRS

De Dino Esposito

Dino EspositoO mundo está repleto de sistemas CRUD (Create, Read, Update, Delete, Criar, Ler, Atualizar, Excluir) clássicos criados com base em um banco de dados relacional e preenchidos com trechos de lógica de negócios, algumas vezes enterrados em procedimentos armazenados e, em outras, presos em componentes de caixa preta. No núcleo dessas caixas pretas, existem quatro operações CRUD: criação de novas entidades, leitura, atualização e exclusão. Em um nível suficientemente alto de abstração, é só isso, todos os sistemas são, de alguma forma, sistemas CRUD. Às vezes, as entidades podem ser muito complexas e assumem o formato de uma agregação.

No DDD (Domain-Driven Design, Design Orientado a Domínio), uma agregação é um cluster relacionado a negócios de entidades com um objeto raiz. Dessa forma, a criação, a atualização ou até a exclusão de uma entidade podem estar sujeitas a várias regras de negócios complexas. Até a leitura do estado de uma agregação normalmente é problemática, principalmente por causa da Experiência do Usuário. O modelo que funciona para alterar o estado do sistema não é, necessariamente, o mesmo modelo que funciona para a apresentação de dados para os usuários em todos os casos.

Levar a abstração de CRUD ao mais alto nível, leva diretamente à separação do que altera o estado de um sistema daquilo que simplesmente retorna uma ou mais exibições dele. Esta é a essência bruta do CQRS (Command and Query Responsibility Segregation), especialmente a segregação pura de responsabilidades de comando e de consulta.

Entretanto, há mais nele do que aquilo que os arquitetos de software e os desenvolvedores devem considerar. O estado do sistema é alterado na pilha de comandos e, em termos concretos, é onde as agregações são criadas pela primeira vez. Também é onde as mesmas agregações são posteriormente atualizadas e excluídas. E esse é exatamente o ponto a ser repensado.

A preservação do histórico é fundamental para quase todos os sistemas de software. Um software é escrito para dar suporte a negócios contínuos, aprender com o passado é fundamental por dois motivos: evitar a perda de alguma coisa que tenha acontecido e aprimorar os serviços para clientes e funcionários.

No episódio de maio de 2016 (msdn.com/magazine/mt703431) e de junho de 2016 (msdn.com/magazine/mt707524) desta coluna, apresento maneiras de estender o CRUD clássico para um CRUD histórico. Entretanto, em minhas colunas de agosto de 2016 (msdn.com/magazine/mt767692) e de outubro de 2016 (msdn.com/magazine/mt742866), apresentei um padrão ECS (Event-Command-­Saga) e uma estrutura Memento FX (bit.ly/2dt6PVD) como os blocos de construção de uma nova forma de expressar a lógica de negócios que atende às necessidades diárias.

Nesta coluna e na próxima, tratarei dos dois benefícios mencionados anteriormente sobre a preservação do histórico em um sistema ao reescrever um aplicativo de demonstração de reserva (o mesmo que usei nas colunas de maio e de junho) com o CQRS e o evento de origem.

O quadro geral

Meu aplicativo de exemplo é um sistema de reserva interno para salas de reunião. O principal caso de uso é um usuário conectado que faz uma rolagem em um calendário e reserva um ou mais slots em uma determinada sala. O sistema gerencia entidades como Room, RoomConfiguration e Booking e, como você pode imaginar, conceitualmente o aplicativo inteiro trata da adição e da edição de salas e de configurações (ou seja, quando a sala está aberta para reserva e para o comprimento de slots simples) e da adição, atualização e cancelamento de reservas. A Figura 1 oferece uma visão rápida das ações que os usuários do sistema podem executar e como elas serão arquitetadas em um sistema CQRS de acordo com o padrão ECS.

Ações do Usuário e Design de Alto Nível do Sistema
Figura 1 Ações do Usuário e Design de Alto Nível do Sistema

Um usuário pode inserir uma nova reserva, mover e cancelá-la, além de fazer check-in na sala de forma que o sistema saiba que a sala reservada está sendo realmente usada. O fluxo de trabalho por trás de cada ação é tratado em uma saga e a saga é uma classe definida na pilha de comandos. Uma classe saga é composta por métodos do manipulador e cada um processa um comando ou um evento. Fazer uma reservar (ou mover uma reserva existente) é uma questão de enviar por push um comando para a pilha de comandos. De forma geral, enviar um comando por push pode ser tão simples quanto invocar o método saga correspondente ou ele pode passar pelos serviços de um barramento.

Para preservar o histórico, você precisa controlar, pelo menos, todos os efeitos de negócios de todos os comandos processados. Em alguns casos, talvez você queira controlar os comandos originais. Um comando é um objeto de transferência de dados que transporta alguns dados de entrada. Um efeito de negócios da execução de um comando por meio de uma saga é um evento. Um evento é um objeto de transferência de dados que carrega os dados que descrevem totalmente o evento. Os eventos são salvos em um armazenamento de dados específico. Não há restrições rígidas sobre a tecnologia de armazenamento a ser usada para os eventos. Ela pode ser um RDBMS simples ou um armazenamento de dados NoSQL. (Consulte a coluna de outubro da configuração do MementoFX e RavenDB e barramento).

Coordenando Comandos e Consultas

Digamos que um usuário execute um comando para reservar um slot em uma determinada sala. Em um cenário do ASP.NET, o controlador obtém os dados postados e executa um comando para o barramento. O barramento é configurado para reconhecer algumas sagas e cada saga declara os comandos (e/ou eventos) em que está interessada em manipular. Dessa forma, o barramento despacha a mensagem para a saga. A entrada da saga é composta pelos dados brutos que os usuários digitaram nos formulários da interface do usuário. O manipulador da saga é responsável pela transformação dos dados recebidos em uma instância de uma agregação consistente com a lógica de negócios.

Digamos que o usuário clique para reservar, como mostrado na Figura 2. O método do controlador disparado pelo botão recebe a ID da sala, o dia e a hora, além do nome do usuário. O manipulador de sagas deve transformar isso em uma agregação de reserva ajustada para lidar com a lógica de negócios esperada. A lógica de negócios tratará de forma sensata os problemas na área de permissões, prioridades, custos e até de concorrência simples. Entretanto, o método da saga terá de criar pelo menos uma agregação de reserva e salvá-la.

Reserva de uma Sala de Reunião no Sistema de Exemplo
Figura 2 Reserva de uma Sala de Reunião no Sistema de Exemplo

À primeira vista, o trecho de código na Figura 3 não é diferente de um CRUD simples, exceto por seu uso em um alocador e na propriedade Repository pendente. O efeito combinado de gravações no alocador e no repositório no evento configurado armazena todos os eventos disparados na implementação da classe Booking.

Figura 3 Estrutura de uma Classe 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);
  }
}

No final, o repositório não salva um registro com o estado atual de uma classe Booking onde as propriedades estejam de alguma forma mapeadas para colunas. Ele simplesmente salva os eventos de negócios no armazenamento e, no final desse estágio, você saberá exatamente o que aconteceu à sua reserva (quando foi criada e como), mas não terá todas as informações clássicas prontas para serem exibidas para o usuário. Você sabe o que aconteceu, mas não tem nada pronto para mostrar. O código-fonte do alocador é mostrado na Figura 4.

Figura 4 Código-fonte do Alocador

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

Nenhuma propriedade da instância recém-criada da classe Booking será modificada no alocador. No entanto, uma classe de evento é criada e preenchida com os dados reais que serão armazenados na instância, incluindo o nome em maiúsculas do cliente e a ID exclusiva que controlará a reserva no sistema de forma permanente. O evento é passado para o método RaiseEvent, parte da estrutura MementoFX, já que é a classe base de todas as agregações. RaiseEvent adiciona o evento a uma lista interna pela qual o repositório passará ao “salvar” a instância da agregação. Eu usei o termo “salvar” porque isso é o que acontece, mas coloquei entre aspas para enfatizar que é um tipo diferente de ação de um CRUD clássico. O repositório salva o evento em que uma reserva foi criada com os dados especificados. Mais precisamente, o repositório salva todos os eventos registrados em uma instância da agregação durante a execução de um fluxo de trabalho, isto é, um método de manipulador de saga, como mostrado na Figura 5.

Salvar Eventos Versus Salvar um Estado
Figura 5 Salvar Eventos Versus Salvar um Estado

Porém, apenas acompanhar o evento de negócios resultante de um comando não é suficiente.

Desnormalizar Eventos para a Pilha de Consulta

Se você examinar o CRUD pelas lentes da preservação do histórico de dados, verá que a criação e a leitura de entidades não afeta o histórico, mas o mesmo não pode ser dito para a atualização e a exclusão. Um repositório de eventos é somente de acréscimo e as atualizações e exclusões são apenas novos eventos relacionados às mesmas agregações. Entretanto, ter uma lista de eventos para uma determinada agregação informa você tudo sobre o histórico, exceto o estado atual. E o estado atual é exatamente o que você precisa apresentar para os usuários.

Veja onde os desnormalizadores se encaixam. Um desnormalizador é uma classe criada na forma de um conjunto de manipuladores de eventos, iguais aos que estão sendo salvos no repositório de eventos. Você registra um desnormalizador no barramento e o barramento despacha os eventos para ele sempre que obtiver um. O efeito líquido é que um desnormalizador gravado para ouvir o evento de uma reserva criado terá a chance de reagir sempre que um for disparado.

Um desnormalizador obtém os dados no evento e faz o que for necessário, por exemplo, manter um banco de dados relacional fácil de consultar em sincronia com eventos registrados. O banco de dados relacional (ou um repositório ou cache NoSQL, se for mais fácil ou mais benéfico de usar) pertence à pilha de consultas e sua API não tem acesso à lista de eventos armazenada. Além disso, você pode ter vários desnormalizadores criando exibições ad hoc dos mesmos eventos brutos. (Detalharei um pouco mais esse aspecto em minha próxima coluna). Na Figura 1, o calendário do qual um usuário escolhe um slot é populado de um banco de dados relacional simples que é mantido em sincronia com eventos pela ação de um desnormalizador. Veja a Figura 6 para obter o código da classe de desnormalizador.

Figura 6 Estrutura de uma Classe 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();
    }  }
  ...
}

Em relação à Figura 5, os desnormalizadores oferecem um CRUD relacional somente para fins de leitura. Com frequência, a saída de desnormalizadores é chamada de “modelo de leitura”. As entidades do modelo de leitura normalmente não correspondem às agregações usadas para gerar eventos já que, em sua maioria, são orientadas pelas necessidades da interface do usuário.

Atualizações e Exclusões

Suponha agora que o usuário queira mover um slot reservado anteriormente. É executado um comando com todos os detalhes do novo slot e um método saga cuida da gravação de um evento Moved para a reserva determinada. A saga precisa recuperar a agregação e precisa dela em estado atualizado. Se os desnormalizadores tiverem acabado de criar uma cópia relacional do estado da agregação (para que o modelo de leitura quase coincida com o modelo de domínio), você poderá obter o estado atualizado daí. Caso contrário, crie uma cópia nova da agregação e execute todos os eventos registrados em log nela. No final da reprodução, a agregação estará no estado mais atualizado. A reprodução de eventos não é uma tarefa que tenha que ser executada diretamente. No MementoFX, você obtém uma agregação atualizada com uma linha de código em um manipulador de sagas:

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

Em seguida, você aplica à instância qualquer lógica de negócios necessária. A lógica de negócios gera eventos e os eventos são persistidos pelo repositório:

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

Se você usar o padrão Modelo de Domínio e seguir os princípios DDD, o método Move conterá toda a lógica e os eventos do domínio. Caso contrário, você executa uma função com qualquer lógica de negócios e gera eventos diretamente para o barramento. Ao associar outro manipulador de eventos ao desnormalizador, você terá a chance de atualizar o modelo de leitura.

A abordagem não é diferente para o cancelamento de uma reserva. O evento de cancelamento de uma reserva é um evento de negócios e deve ser controlado. Isso significa que talvez você queira ter uma propriedade booliana na agregação para executar a exclusão lógica. No entanto, no modelo de leitura, a exclusão poderia ser divinamente física, dependendo de onde seu aplicativo consultará as reservas canceladas no modelo de leitura. Um efeito colateral interessante é que você sempre pode recriar o modelo de leitura ao reproduzir eventos desde o início ou de um ponto de recuperação. Basta criar uma ferramenta ad hoc que usa a API do repositório de eventos para ler eventos e chamar diretamente os desnormalizadores.

Uso da API do Repositório de Eventos

Veja a seleção da lista suspensa na Figura 2. O usuário deseja alongar a reserva o máximo possível desde a hora de início. A lógica de negócios na agregação deve ser capaz de entender isso e realizar a tarefa e, portanto, deve acessar a lista de reservas no mesmo dia após a hora de início. Não há muita coisa em um CRUD clássico, mas o MementoFX também permite que você consulte eventos:

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

O trecho de código retorna uma lista de eventos NewBookingCreated após o horário determinado. Entretanto, não há garantias de que a reserva criada ainda esteja ativa e não tenha sido movida para outro slot. Você realmente precisa obter o estado atualizado dessas agregações. Você é quem escolhe o algoritmo. Por exemplo, você pode filtrar na lista de eventos Created as reservas que não estão mais ativas e assim obter a ID das reservas restantes. Por fim, verifique o slot real em relação ao que você deseja alongar ao mesmo tempo que evita a sobreposição. No código-fonte deste artigo, codifiquei toda essa lógica em um serviço separado (domínio) na pilha de comandos.

Conclusão

O uso do CQRS e do evento de origem não é limitado a determinados sistemas com requisitos de alto nível para simultaneidade, escalabilidade e desempenho. Com uma infraestrutura disponível que permite trabalhar com agregações e fluxos de trabalho, qualquer um dos sistemas CRUD atuais pode ser reescrito de forma a trazer mais benefícios. Esses benefícios incluem:

  • Preservação do histórico de dados
  • Uma maneira mais efetiva e resiliente de implementar tarefas de negócios e de alterar tarefas para refletir as alterações de negócios com esforço e risco de regressão limitados
  • Como os eventos são fatos imutáveis, é trivial copiá-los e duplicá-los. E até os modelos de leitura podem ser regenerados programaticamente à vontade

Isso significa que o padrão ECS (ou CQRS/ES, como é mencionado em algumas ocasiões) tem um tremendo potencial para escalabilidade. Além disso, a estrutura MementoFX será útil aqui porque simplifica tarefas comuns e oferece a abstração da agregação para facilitar a programação.

O MementoFX envia por push uma abordagem orientada a DDD, mas você pode usar o padrão ECS com outras estruturas e outros paradigmas, como o paradigma funcional. Há outro benefício e, provavelmente, o mais relevante. Contarei a vocês sobre a minha próxima coluna.


Dino Espositoé o autor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) e de “Modern Web Applications” (Microsoft Press, 2016). Evangelista técnico das plataformas .NET e Android no JetBrains e palestrante frequente em eventos do setor no mundo todo, Esposito compartilha sua visão de software em software2cents.wordpress.com e, no Twitter, em @despos.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Andrea Saltarello