Agosto de 2016

Volume 31, Número 8

Cutting Edge - Fora do CRUD: Comandos, Eventos e Barramento

Por Dino Esposito | Agosto de 2016

Dino EspositoEm outras partes desta coluna, falei sobre o que é necessário para compilar um H-CRUD (criar, ler, atualizar, excluir histórico). Um H-CRUD é uma extensão simples para o CRUD clássico, em que se pode usar dois armazenamentos de dados distintos para persistir o estado atual dos objetos e todos os eventos que ocorreram durante a vida útil de cada objeto. Se você apenas limitar sua visão ao armazenamento de dados que contém o estado atual, fica tudo basicamente igual ao CRUD clássico. Você tem seus registros de cliente, suas faturas, seus pedidos e o que mais fizer parte do modelo de dados do domínio do negócio.

O principal aqui é que o armazenamento de dados resumido não é o armazenamento de dados principal que você cria, mas sim uma derivação como projeção do armazenamento de dados de eventos. Melhor dizendo, a essência de criar um CRUD histórico é salvar os eventos conforme vão ocorrendo para depois inferir o estado atual do sistema para qualquer interface do usuário que você precise criar.

 Projetar sua solução em torno de eventos de negócios é uma abordagem relativamente nova que está ganhando força, embora ainda falte um pouco para ela se tornar um paradigma consagrado. Centralizar seu design em eventos é bom porque você jamais perde algo que ocorre no sistema: é possível reler e reproduzir eventos a qualquer hora e criar novas projeções em cima dos mesmos dados para fins de business intelligence, por exemplo. Mais interessante ainda é que, usando eventos como arquiteto, você é capaz de projetar o sistema em torno de linguagem ubíqua e específica para o negócio. Além de ser um pilar de DDD (design orientado pelo domínio), a linguagem ubíqua é, em termos mais práticos, uma grande ajuda para entender o domínio de negócios e planejar o diagrama arquitetônico mais eficaz de partes colaboradoras e dinâmicas internas de tarefas e fluxos de trabalho.

Você já deve ter lido sobre implementação de eventos nas minhas colunas de maio (msdn.com/magazine/mt703431) e junho de 2016 (msdn.com/magazine/mt707524); nelas, a implementação era muito simples. Até simplista, diria. O objetivo principal era mostrar que qualquer CRUD pode ser transformado em um H-CRUD com um mínimo de esforço e ainda obter algumas vantagens com a introdução de eventos de negócio. A abordagem de H-CRUD tem algumas sobreposições óbvias com siglas e palavras-chave populares hoje em dia, como CQRS e Event Sourcing. Nesta coluna, vou aprofundar a ideia de H-CRUD para juntá-la à ideia principal de Event Sourcing. Você verá como um H-CRUD pode se transformar em uma implementação composta por comandos, barramentos e eventos que, a princípio, parecerão uma maneira muito complexa de fazer leituras e gravações básicas em um banco de dados.

Um evento, várias agregações

Na minha opinião, um dos motivos de ser difícil às vezes escrever software dentro do prazo e dentro do orçamento é a falta de atenção à linguagem de negócio usada pelo especialista do domínio. Na maior parte do tempo, o reconhecimento dos requisitos significa o mapeamento dos requisitos compreendidos para algum tipo de modelo de dados relacional. A lógica do negócio é usada para criar um tunelamento de dados entre persistência e apresentação, com os devidos ajustes ao longo do caminho. Embora seja imperfeito, esse padrão funcionou por um bom tempo, e a quantidade de casos em que os níveis gigantescos de complexidade o tornaram impraticável eram estatisticamente irrelevantes e, de qualquer forma, levaram à criação do DDD, ou seja, ainda é a maneira mais eficaz para lidar com projetos de software hoje em dia.

Eventos são bons nesse caso porque forçam uma maneira diferente de analisar o domínio, muito mais voltada para tarefas e sem a urgência de criar o modelo relacional perfeito para salvar dados. No entanto, ao se olhar para eventos, a cardinalidade é essencial. Nos exemplos de H-CRUD que abordei em colunas anteriores, assumi uma premissa que, sem maiores explicações e considerações, pode ser nociva. Nos meus exemplos, usei uma associação evento-para-agregação um-para-um. Na verdade, usei o identificador exclusivo da agregação que era persistida como chave estrangeira para eventos de link. Para usar o exemplo do artigo, sempre que um quarto é reservado, o sistema registra em log um evento criado por reserva que se refere a uma ID de reserva específica. Para recuperar todos os eventos de uma agregação (ou seja, a reserva), uma consulta ao armazenamento de dados de eventos da ID de reserva será suficiente para obter todas as informações. Isso realmente funciona, mas é um cenário extremamente simples. O perigo está no fato de que, quando aspectos de um cenário simples se tornam prática comum, você passa de uma solução simples a uma solução simplista. E isso não é necessariamente uma boa coisa.

Agregações e objetos

A verdadeira cardinalidade da associação evento/agregação é escrita na linguagem ubíqua do domínio do negócio. De qualquer maneira, uma associação um-para-vários tem muito mais chances de acontecer do que uma simples associação um-para-um. Falando concretamente, uma associação um-para-vários entre eventos e agregações significa que às vezes um evento pode ser relevante para várias agregações e mais de uma agregação pode estar interessada em processar o evento e poderá ter seu estado alterado por causa de tal evento.

Como exemplo, imagine um cenário em que uma fatura é registrada no sistema como um custo de pedido de trabalho em andamento. Isso significa que, em seu modelo de domínio, você provavelmente terá duas agregações, fatura e pedido de trabalho. O evento de registro de fatura chama o interesse da agregação fatura porque uma nova fatura é inserida no sistema, mas também poderá chamar a atenção da agregação JobOrder se a fatura for sobre alguma atividade relevante para o pedido. É claro que a fatura estar relacionada a um pedido de trabalho ou não somente pode ser determinado depois da compreensão total do domínio do negócio. Pode haver modelos de domínio (e aplicativos) em que uma fatura tenha seu lugar autônomo e modelos de domínio (e aplicativos) em que uma fatura possa ser registrada na contabilidade de um pedido de trabalho e, consequentemente, alterar o saldo atual.

No entanto, entender que os eventos podem se relacionar com várias agregações muda completamente a arquitetura da solução e até mesmo o panorama das tecnologias viáveis.

A expedição de eventos desfaz a complexidade

Um elemento básico de CRUD e H-CRUD é a grande limitação que é os eventos estarem vinculados a uma única agregação. Quando várias agregações são tocadas por um evento de negócios, você escreve código de lógica de negócios para garantir que o estado será alterado e controlado da maneira adequada. Quando a quantidade de agregações e eventos ultrapassa um limite crítico, a complexidade do código de lógica de negócios pode ficar muito grande e inviabilizar seu tratamento e evolução.

Nesse contexto, o padrão de CQRS representa um primeiro passo na direção certa, já que ele recomenda raciocínios diferentes para ações que “só leem” ou “só alteram” o estado atual do sistema. Event Sourcing é outro padrão popular que sugere o registro em log de tudo que ocorre no sistema como evento. Todo o estado do sistema é controlado e o estado real das agregações no sistema é criado como uma projeção dos eventos. Melhor dizendo, você mapeia o conteúdo dos eventos para outras propriedades que, juntas, formam o estado dos objetos que podem ser usados no software. O Event Sourcing é criado em torno de uma estrutura que sabe como salvar e recuperar eventos. Um mecanismo de Event Sourcing é somente acréscimo, dá suporte à reprodução de transmissão de eventos e sabe como salvar dados relacionados que podem ter layouts totalmente diferentes.

Estruturas de armazenamento de dados como EventStore (bit.ly/1UPxEUP) e NEventStore (bit.ly/1UdHcfz) abstraem a verdadeira estrutura de persistência e oferecem uma super API para lidar com os eventos diretamente em código. No fundo, você vê transmissões de eventos que estão de alguma forma relacionados e o ponto de atração desses eventos é uma agregação. Isso funciona muito bem. No entanto, quando um evento tem impacto em várias agregações, você deve encontrar uma maneira de dar a cada agregação a capacidade de rastrear todos os eventos de interesse. Além disso, você deve conseguir criar uma infraestrutura de software que, além da mera persistência dos pontos de eventos, permita que todas as agregações sejam informadas de eventos de interesse.

Para atingir as metas de expedir eventos para agregações e persistir os eventos corretamente, o H-CRUD não será suficiente. Tanto o padrão por trás da lógica de negócios quanto a tecnologia usada para persistir dados relacionados a eventos devem ser revisitados.

Definindo a agregação

O conceito de uma agregação vem do DDD e, resumindo, ele se refere a um cluster de objetos de domínio agrupados para corresponder à consistência transacional. A consistência transacional significa simplesmente que tudo o que for parte de uma agregação seja garantido como consistente e atualizado no fim de uma ação de negócios. O trecho de código a seguir apresenta uma interface que resume os principais aspectos de praticamente todas as classes de agregação. Pode haver outros, mas eu diria que isso é o mínimo:

public interface IAggregate
{
  Guid ID { get; }
  bool HasPendingChanges { get; }
  IList<DomainEvent> OccurredEvents { get; set; }
  IEnumerable<DomainEvent> GetUncommittedEvents();
}

A agregação sempre contém a lista dos eventos ocorridos e pode distinguir entre os confirmados e os não confirmados que podem resultar em cargas pendentes. Uma classe de base para implementar a interface IAggregate terá um membro não público para definir a ID e implementar a lista de eventos confirmados e não confirmados. Além disso, a classe base Aggregate também terá métodos RaiseEvent usados para adicionar um evento à lista interna de eventos não confirmados. O interessante é como os eventos são usados internamente para alterar o estado de uma agregação. Digamos que você tem uma agregação Cliente e deseja atualizar o nome público do cliente. Em um cenário de CRUD, isso será uma atribuição basicamente assim:

customer.DisplayName = "new value";

Com eventos, o roteiro será mais sofisticado:

public void Handle(ChangeCustomerNameCommand command)
{
 var customer = _customerRepository.GetById(command.CompanyId);
 customer.ChangeName(command.DisplayName);
 customerRepository.Save(customer);
}

Vamos ignorar o método Handle e quem o executa no momento e focar na implementação. Inicialmente, parece que ChangeName é apenas um encapsulador para o código estilo CRUD examinado anteriormente. Bem, não exatamente:

public void ChangeName(string newDisplayName)
{
  var evt = new CustomerNameChangedEvent(this.Id, newDisplayName);
  RaiseEvent(e);
}

O método RaiseEvent definido na classe base Aggregate apenas adicionará o evento à lista interna de eventos não confirmados. Os eventos não confirmados são finalmente processados quando a agregação é persistida.

Persistindo o estado por meio de eventos

Com o grande envolvimento de eventos, a estrutura das classes de repositório podem ser generalizadas. O método Save de um repositório projetado para operar com as classes de agregação descritas até o momento simplesmente executará loop na lista de eventos não confirmados da agregação e chamará um novo método que a agregação tem que oferecer, o método ApplyEvent:

public void ApplyEvent(CustomerNameChangedEvent evt)
{
  this.DisplayName = evt.DisplayName;
}

A classe de agregação terá uma sobrecarga do método ApplyEvent para cada evento de interesse. O código estilo CRUD que você viu lá atrás tem lugar aqui.

Ainda há uma ponta solta: Como você orquestra casos de uso de front-end e ações de usuário final com várias agregações, negócios, fluxos de trabalho e persistência? Você precisa de um componente de barramento.

Introduzindo o componente de barramento

Um componente de barramento pode ser definido como um caminho compartilhado entre instâncias em execução de processos de negócio conhecidos. Os usuários finais atuam na camada de apresentação e definem instruções com as quais o sistema deve lidar. A camada do aplicativo recebe essas entradas e as transforma em ações de negócios concretas. Em um cenário CRUD, a camada de aplicativo chamará diretamente o processo de negócios (ou seja, o fluxo de trabalho) responsável pela ação solicitada.

Quando as agregações e as regras de negócios são muitas, um barramento simplifica bastante o design geral. A camada de aplicativo envia um comando ou evento por push ao barramento para que os ouvintes reajam de acordo. Ouvintes são componentes normalmente chamados de “sagas”, que são, no fim das contas, instâncias de processos de negócios conhecidos. Uma saga sabe como reagir a vários comandos e eventos. Uma saga tem acesso à camada de persistência e pode devolver comandos e eventos por push ao barramento. A saga é a classe a que pertence o método Handle mencionado anteriormente. Normalmente existe uma classe de saga por fluxo de trabalho ou caso de uso, e uma saga é totalmente identificada pelos eventos e comandos que ela pode tratar. A arquitetura resultante no geral é exibida na Figura 1.

Usando um barramento para expedir eventos e comandos
Figura 1 - Usando um barramento para expedir eventos e comandos

Por fim, observe que os eventos também devem ser persistidos e consultados desde a origem. Isso levanta outra questão relevante: um banco de dados relacional clássico é ideal para armazenar eventos? Eventos diferentes podem ser adicionados a qualquer momento durante o desenvolvimento e até após a produção. Cada evento, além disso, tem seu próprio esquema. Nesse contexto, um armazenamento de dados não relacional é adequado, mesmo que o uso de um banco de dados relacional ainda seja uma opção. Pelo menos uma opção a considerar e descartar dependendo de provas substanciais.

Conclusão

Ouso dizer que grande parte da aparente complexidade do software é devido ao fato de que usamos a forma CRUD de pensar para sistemas que, embora se baseiem nas quatro operações fundamentais da sigla (criar, ler, atualizar, excluir), não são mais tão simples quanto ler e gravar em uma única tabela ou agregação. Este artigo é apenas um aperitivo em relação a análises mais detalhadas de padrões e ferramentas, que continuaremos no mês que vem quando eu apresentar uma estrutura que tenta tornar esse tipo de desenvolvimento mais rápido e sustentável.


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 ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Jon Arne Saeteras