Março de 2016

Volume 31 Número 3

Cutting Edge - a pilha de consulta de uma arquitetura CQRS

Por Dino Esposito | Março de 2016

Dino EspositoHoje, a Segregação de Responsabilidade de Consulta e Comando (CQRS) é um dos tópicos de arquitetura mais comentados. Em seu núcleo, a CQRS é nada mais do que o senso comum e tudo o que ela recomenda é que você codifique as consultas e os comandos exigidos por seu sistema por meio de pilhas distintas e ad hoc. Talvez você queira uma camada de domínio na pilha de comandos e organizar as tarefas corporativas em serviços de domínio, bem como empacotar as regras de negócios em objetos de domínio. A camada de persistência também pode ser codificada com total liberdade, simplesmente escolhendo a tecnologia e a abordagem mais adequada às suas necessidades, sejam repositórios básicos de tabelas relacionais, NoSQL ou de evento.

E quanto à pilha de leitura? A boa notícia é que assim que você começa a examinar a arquitetura CQRS, não sente com tanta frequência a necessidade de ter uma seção distinta da camada de domínio somente para ler os dados. As consultas básicas de tabelas prontas são tudo o que você precisa. A pilha de consulta executa operações somente leitura no back-end e que não alteram o estado do sistema. Por causa disso, talvez você não precise de qualquer forma de regras de negócios e de intermediação lógica entre a apresentação e o armazenamento — ou pelo menos nada além dos recursos básicos dos operadores de consulta avançada do SQL, como GROUP BY, JOIN e ORDER. Na implementação de uma pilha de consulta moderna, a linguagem LINQ com base no Microsoft .NET Framework é bastante útil. No restante desta coluna, analisarei uma possível implementação de uma pilha de leitura, em que o armazenamento foi projetado para estar próximo da organização dos dados exigidos pela apresentação.

Ter dados prontos

Em um projeto CQRS, normalmente você planeja projetos separados para as pilhas de consulta e de comandos, e pode até ter equipes diferentes cuidando de cada pilha. Essencialmente, uma pilha de leitura consiste em dois componentes: uma coleção de DTOs (Objetos de Transferência de Dados) para entregar dados para a camada de apresentação e um contexto de banco de dados para executar leituras físicas e preencher os DTOs.

Para manter a pilha de consulta o mais simples possível, você deve tentar ter uma correspondência próxima entre os dados armazenados e os dados a apresentar. Especialmente hoje, este pode não ser o caso: É mais comum o cenário no qual os dados são idealmente armazenados em um determinado formato, mas que devem ser organizados em um formato bem diferente para serem consumidos de forma efetiva. Como exemplo, pense em um sistema de software para reservar salas de reunião em um ambiente corporativo. A Figura 1 oferece uma perspectiva gráfica deste cenário.

Esquema de reserva em uma arquitetura CQRS
Figura 1 - Esquema de reserva em uma arquitetura CQRS

São oferecidas ao usuário algumas formas para a criação de uma solicitação de reserva ou para a atualização de uma reserva existente. Cada ação é registrada em log no repositório de dados de comandos como está. Rolando pela lista de eventos de reserva registrados, é possível pode controlar com facilidade quando cada reserva foi inserida no sistema, quando foi modificada, quantas vezes foi modificada e quando foi excluída, se for o caso. Essa é, definitivamente, a maneira mais eficaz de armazenar informações sobre um sistema dinâmico onde o estado dos itens armazenados pode mudar com o tempo.

O salvamento do estado do sistema no formato de eventos tem muitos benefícios, conforme resumido na minha coluna Cutting Edge de agosto de 2015 (msdn.com/magazine/mt185569), mas não oferece uma exibição imediata do estado atual do sistema. Em outras palavras, talvez você possa obter o histórico completo de uma reserva única, mas não poderá retornar imediatamente a lista de reservas pendentes. No entanto, os usuários do sistema também estão normalmente interessados em obter a lista de reservas. Isso cria a necessidade de dois repositórios diferentes, mantidos em sincronia. Sempre que um novo evento for registrado em log, o estado das entidades envolvidas deverá ser atualizado (de forma síncrona ou assíncrona) para facilitar a consulta.

Durante a etapa de sincronização, verifique se os dados úteis foram extraídos do log de eventos e moldados em um formato fácil de consumir da interface do usuário por meio da pilha de consulta. Neste ponto, tudo o que a pilha de consulta precisa fazer é formatar os dados para o modelo de dados em particular da exibição atual.

Mais sobre a função de eventos

Uma objeção comum é “Por que devo salvar os dados no formato de eventos? Por que não posso simplesmente salvar os dados no formato em que eles serão usados?”

A resposta é clássica, “Depende”, e o aspecto interessante é que isso normalmente não depende de você, o arquiteto. Isso depende das necessidades de negócios. Se for fundamental para o cliente controlar o histórico de um item de negócios (ou seja, a reserva) ou ver qual era a lista de reservas em um determinado dia, e caso a disponibilidade de quartos possa mudar com o tempo, a maneira mais simples de resolver o problema é registrar eventos em log e criar toda a projeção de dados necessária de acordo com os logs.

A propósito, essa também é a mecânica interna usada pelos serviços e pelos aplicativos de business intelligence (BI). Ao usar os eventos, você pode até estabelecer a fundação de alguma BI interna.

Contexto somente leitura do Entity Framework

O Entity Framework é uma maneira comum de acessar dados armazenados, pelo menos de dentro de aplicativos .NET e ASP.NET. O Entity Framework afunila chamadas de banco de dados por meio de uma instância da classe DbContext. A classe DbContext oferece acesso de leitura/gravação para o banco de dados subjacente. Se você tornar a instância de DbContext visível nas camadas superiores, está expondo sua arquitetura ao risco de também receber atualizações de estado de dentro da pilha de consulta. Isso não é um bug propriamente dito, mas é uma violação séria de regras de arquitetura que você deve evitar. Para evitar o acesso de gravação ao banco de dados, talvez você queira encapsular a instância do DbContext em um contêiner e em uma classe descartável, conforme mostrado na Figura 2.

Figura 2 - encapsulamento da instância de DbContext em um contêiner e em uma classe descartável

public class Database : IDisposable
{
  private readonly SomeDbContext _context = new SomeDbContext();
  public IQueryable<YourEntity> YourEntities
  {
    get
    {
      return _context.YourEntities;
    }
  }
  public void Dispose()
  {
    _context.Dispose();
  }
}

Você pode usar um Banco de Dados sempre que quiser usar um DbContext só para consultar os dados. Há dois aspectos que diferenciam a classe Banco de Dados de uma DbContext básica. O primeiro e mais importante é que a classe Banco de Dados encapsula uma instância de DbContext como um membro privado. O segundo é que a classe Banco de Dados expõe todas ou algumas das coleções DbContext como coleções IQueryable<T> em vez de coleções DbSet<T>. Esse é o truque para aproveitar o poder de consulta do LINQ, mesmo não sendo capaz de adicionar, excluir ou simplesmente salvar as alterações novamente.

Modelagem de dados para a exibição

Em um arquitetura CQRS, não há uma posição canônica quanto à organização da camada de aplicativos. Pode ser diferente para as pilhas de comandos e de consulta ou pode ser uma camada única. Na maior parte do tempo, a decisão é tomada de acordo com a visão do arquiteto. Se você conseguiu manter o armazenamento duplo e, portanto, tem dados sob medida para apresentação, então quase não há a necessidade de uma lógica complexa de recuperação de dados. Subsequentemente, a implementação da pilha de consulta pode ser mínima. Na parte de consulta do código da camada de aplicativos, você poderá ter código de acesso direto aos dados e chamar diretamente o ponto de entrada de DbContext do Entity Framework. Entretanto, para manter tudo realmente limitado às operações de consulta, bastará usar a classe Banco de Dados em vez da DbContext nativa. Dessa forma, tudo que você obtiver da classe Database só será consultável por meio do LINQ. A Figura 3 oferece um exemplo.

Figura 3 - uma consulta da classe Banco de Dados por meio do LINQ

using (var db = new Database())
{
  var queryable = from i in db.Invoices
                              .Include("Customers")
    where i.InvoiceId == invoiceId
    select new InvoiceFoundViewModel
    {
      Id = i.InvoiceId,
      State = i.State.ToString(),
      Total = i.Total,
      Date = i.IssueDate,
      ExpiryDate = i.GetExpiryDate()
    };
    // More code here
}

Como você pode ver, a projeção real dos dados que você obteria da consulta é especificada no último minuto, diretamente na camada de aplicativos. Isso significa que você pode criar o objeto IQueryable em algum lugar da camada de infraestrutura e movê-lo pelas camadas. Cada camada tem a oportunidade de modificar ainda mais o objeto ao refinar a consulta sem realmente executá-la. Ao fazer isso, você não precisará criar toneladas de objetos de transferência para mover os dados entre as camadas.

Vamos considerar uma consulta razoavelmente complexa. Digamos, por exemplo, que para um dos casos de uso no aplicativo, você precisará recuperar todas as faturas de uma unidade de negócios que ainda não foram pagas, 30 dias após a data de vencimento. Se a consulta tivesse expressado uma necessidade de negócios estável e consolidada, não sujeita a mais adaptações ao longo do caminho, ela não seria uma consulta difícil de escrever em T-SQL básico. A consulta é composta de três partes principais: obter todas as faturas, selecionar os aspectos específicos de uma determinada unidade de negócios e selecionar aquelas que ainda não foram pagas após 30 dias. De uma perspectiva de implementação, não faz nenhum sentido dividir a consulta original em três subconsultas e fazer a maior parte do trabalho em memória. Ainda assim, de uma perspectiva conceitual, a divisão da consulta em três partes facilita a compreensão, especialmente para os novatos em domínios.

O LINQ é uma ferramenta mágica que permite expressar a consulta conceitualmente e executá-la em uma única etapa, em que o provedor LINQ subjacente lida com a tradução da consulta na linguagem de consulta adequada. Veja uma maneira possível de expressar a consulta LINQ:

var queryable = from i in db.Invoices
                            .Include("Customers")
  where i.BusinessUnitId == buId &&
     DateTime.Now – i.PaymentDueBy > 30
                select i;

No entanto, a expressão não retorna dados. A expressão simplesmente retorna uma consulta que ainda não foi executada. Para executar a consulta e obter os dados relacionados, você deverá invocar um executor LINQ, como ToList ou FirstOrDefault. A consulta só será criada nesse ponto, juntando todas as partes e retornando os dados.

O LINQ e a linguagem ubíqua

Um objeto iQueryable pode ser modificado ao longo do caminho. Faça isso ao chamar os métodos Where e Select programaticamente. Dessa maneira, à medida que o objeto iQueryable atravessa as camadas do seu sistema, a consulta final emerge por meio da composição de filtros. E, mais importante, cada filtro é aplicado somente onde a lógica em que ele se baseia está disponível.

Outro ponto relevante sobre o LINQ e sobre objetos iQueryable na pilha de consulta de uma arquitetura CQRS é a legibilidade e a linguagem ubíqua. No DDD (design orientado ao domínio), a linguagem ubíqua é o padrão que sugere que você desenvolva e mantenha um vocabulário de termos de negócios inequívocos (substantivos e verbos) e, mais importante, que reflita esses termos no código real. Um sistema de software que implemente a linguagem ubíqua, por exemplo, não excluirá a ordem, mas a “cancelará” e não emitirá uma ordem, mas fará “check-out” dela.

O maior desafio do software moderno não está nas soluções técnicas, mas na compreensão profunda das necessidades de negócios e em encontrar uma correspondência perfeita entre as necessidades de negócios e o código. Para melhorar a legibilidade de consultas, você poderá misturar objetos IQueryable e os métodos de extensão do C#. Veja como reescrever a consulta anterior para mantê-la muito mais legível:

var queryable = from i in db.Invoices
                            .Include("Customers")
                            .ForBusinessUnit(buId)
                            .Unpaid(30)
                select i;

ForBusinessUnit e Unpaid são dois métodos de extensão definidos pelo usuário que estendem o tipo IQueryable<Invoice>. Tudo o que eles têm de fazer é adicionar uma cláusula WHERE à definição da consulta em curso:

public static IQueryable<Invoice> ForBusinessUnit(
  this IQueryable<Invoice> query, int businessUnitId)
{
  var invoices =
    from i in query
    where i.BusinessUnit.OrganizationID == buId
    select i;
  return invoices;}

De maneira análoga, o método Unpaid consistirá em nada mais do que outra cláusula WHERE para restringir ainda mais os dados retornados. A consulta final é igual, mas a expressão dela é muito mais clara e compreensível. Em outras palavras, por meio do uso de métodos de extensão, você quase obtém uma linguagem específica do domínio, o que, a propósito, é um dos objetivos da linguagem ubíqua DDD.

Conclusão

Se você comparar o CQRS à DDD básica, verá que, na maioria dos casos, poderá reduzir a complexidade de análise e o projeto de domínio simplesmente ao se concentrar nos casos de uso e nos modelos relacionados que apoiam ações que alteram o estado do sistema. Tudo o mais, especificamente as ações que só leem o estado atual, podem ser expressas por meio de uma infraestrutura de código muito mais simples e podem ser tão complexas como as consultas básicas de banco de dados.

Deve ser observado que, na pilha de consulta de uma arquitetura CQRS, às vezes até mesmo um O/RM completo pode ser demais. No final do dia, tudo o que você precisará fazer é consultar os dados, principalmente de tabelas relacionais prontas. Não há necessidade de coisas sofisticadas como carregamento lento, agrupamento, junções — tudo o que você precisa já está lá, da forma adequada. Um O/RM não trivial como o Entity Framework 6 pode até ser muito para as suas necessidades, e os micro O/RMs, como o PetaPoco ou o NPoco, podem ser suficientes. O interessante é que essa tendência também se reflete parcialmente no design do novo Entity Framework 7 e na nova pilha do ASP.NET 5.


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.*