Projetar a camada de persistência da infraestrutura

Dica

Esse conteúdo é um trecho do eBook da Arquitetura de Microsserviços do .NET para os Aplicativos .NET em Contêineres, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Os componentes de persistência de dados fornecem acesso aos dados hospedados dentro dos limites de um microsserviço (ou seja, o banco de dados de um microsserviço). Eles contêm a implementação real dos componentes, como repositórios e classes Unidade de Trabalho, como objetos DbContext personalizados do EF (Entity Framework). O EF DbContext implementa os padrões de Repositório e de Unidade de Trabalho.

O padrão de repositório

O padrão de Repositório é um padrão de design orientado a domínios destinado a manter as preocupações de persistência fora do modelo de domínio do sistema. Uma ou mais abstrações de persistência (interfaces) são definidas no modelo de domínio e essas abstrações têm implementações na forma de adaptadores específicos de persistência definidos em outro lugar no aplicativo.

As implementações de repositórios são classes que encapsulam a lógica necessária para acessar fontes de dados. Elas centralizam a funcionalidade comum de acesso a dados, aumentando a sustentabilidade e desacoplando a infraestrutura ou a tecnologia usada para acessar os bancos de dados do modelo de domínio. Se você usar um ORM (Mapeador Objeto-Relacional) como o Entity Framework, o código que precisa ser implementado será simplificado, graças à LINQ e à tipagem forte. Isso permite que você se concentre na lógica de persistência de dados e não nos detalhes técnicos do acesso a dados.

O padrão de repositório é uma maneira bem documentada de trabalhar com uma fonte de dados. No livro Padrões de Arquitetura de Aplicações Corporativas, Martin Fowler descreve um repositório da seguinte maneira:

Um repositório executa as tarefas de um intermediário entre as camadas de modelo de domínio e o mapeamento de dados, funcionando de maneira semelhante a um conjunto de objetos de domínio na memória. Os objetos de clientes criam consultas de forma declarativa e enviam-nas para os repositórios buscando respostas. Conceitualmente, um repositório encapsula um conjunto de objetos armazenados no banco de dados e as operações que podem ser executadas neles, fornecendo uma maneira que é mais próxima da camada de persistência. Os repositórios também oferecem a capacidade de separação, de forma clara e em uma única direção, a dependência entre o domínio de trabalho e a alocação de dados ou o mapeamento.

Definir um repositório por agregação

Para cada agregação ou raiz de agregação, você deve criar uma classe de repositório. Talvez você possa aproveitar genéricos do C# para reduzir o número total de classes concretas que precisa manter (conforme demonstrado posteriormente neste capítulo). Em um microsserviço baseado nos padrões de DDD (Design Orientado por Domínio), o único canal que você deve usar para atualizar o banco de dados são os repositórios. Isso ocorre porque há uma relação um-para-um com a raiz agregada que controla as invariantes e a consistência transacional da agregação. É possível consultar o banco de dados por outros canais (como ao seguir uma abordagem de CQRS), porque as consultas não alteram o estado do banco de dados. No entanto, a área transacional (ou seja, as atualizações) sempre precisa ser controlada pelos repositórios e pelas raízes de agregação.

Basicamente, um repositório permite popular na memória dados que são provenientes do banco de dados, em forma de entidades de domínio. Depois que as entidades estão na memória, elas podem ser alteradas e persistidas novamente no banco de dados por meio de transações.

Conforme observado anteriormente, se você estiver usando o padrão de arquitetura CQS/CQRS, as consultas inicias serão executadas por consultas à parte, fora do modelo de domínio, executadas por instruções SQL simples usando o Dapper. Essa abordagem é muito mais flexível do que os repositórios porque você pode consultar e unir as tabelas necessárias, e essas consultas não são restringidas pelas regras das agregações. Esses dados vão para o aplicativo cliente ou de camada de apresentação.

Se o usuário fizer alterações, os dados a serem atualizados virão da camada de apresentação ou do aplicativo cliente para a camada do aplicativo (como um serviço de API Web). Ao receber um comando em um manipulador de comandos, use repositórios para obter os dados que deseja atualizar do banco de dados. Você os atualizará na memória com os dados transmitidos com os comandos e, em seguida, adicionará ou atualizará esses dados (entidades de domínio) no banco de dados por meio de uma transação.

É importante enfatizar novamente que você deve definir apenas um repositório para cada raiz de agregação, conforme mostrado na Figura 7-17. Para atingir a meta da raiz de agregação de manter a consistência transacional entre todos os objetos na agregação, você nunca deve criar um repositório para cada tabela no banco de dados.

Diagram showing relationships of domain and other infrastructure.

Figura 7-17. A relação entre repositórios, agregações e tabelas de banco de dados

O diagrama acima mostra as relações entre as camadas Domínio e Infraestrutura: a Agregação do Comprador depende do IBuyerRepository e a Agregação do Pedido depende das interfaces IOrderRepository, essas interfaces são implementadas na camada Infraestrutura pelos repositórios correspondentes que dependem do UnitOfWork, também implementado lá, que acessa as tabelas na Camada de dados.

Impor uma raiz de agregação por repositório

É importante implementar o design do repositório de uma forma que ele imponha a regra de que apenas as raízes de agregação devem ter repositórios. Você pode criar um tipo de repositório genérico ou de base que restrinja o tipo de entidades com as quais trabalha para garantir que elas tenham a interface de marcador IAggregateRoot.

Assim, cada classe de repositório implementada na camada de infraestrutura implementa seu próprio contrato ou interface, conforme é mostrado no código a seguir:

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
    public class OrderRepository : IOrderRepository
    {
      // ...
    }
}

Cada interface de repositório específica implementa a interface IRepository genérica:

public interface IOrderRepository : IRepository<Order>
{
    Order Add(Order order);
    // ...
}

No entanto, uma maneira melhor de fazer com que o código imponha a convenção de que cada repositório seja relacionado a uma única agregação é implementar um tipo de repositório genérico. Dessa forma, é obrigatório que você esteja usando um repositório para direcionar a uma agregação específica. Isso pode ser feito facilmente com a implementação de uma interface base IRepository genérica, como no código a seguir:

public interface IRepository<T> where T : IAggregateRoot
{
    //....
}

O padrão de repositório facilita os testes da lógica do aplicativo

O padrão de repositório permite testar facilmente o aplicativo com testes de unidade. Lembre-se de que os testes de unidade testam apenas o código, não a infraestrutura, assim, as abstrações de repositório facilitam alcançar essa meta.

Como observado em uma seção anterior, é recomendado que você defina e coloque as interfaces de repositório na camada de modelo de domínio para que a camada de aplicativo, como o microsserviço API Web, não dependa diretamente da camada de infraestrutura em que as classes de repositório reais foram implementadas. Fazendo isso e usando a injeção de dependência nos controladores da API Web, você pode implementar repositórios fictícios que retornam dados falsos em vez de dados do banco de dados. Essa abordagem desacoplada permite que você crie e execute testes de unidade que se focam na lógica do aplicativo sem precisar de conectividade com o banco de dados.

As conexões com bancos de dados podem falhar e, principalmente, executar centenas de testes em relação a um banco de dados é prejudicial por dois motivos. Primeiro, pode demorar muito devido ao grande número de testes. Em segundo lugar, os registros do banco de dados podem mudar e afetar os resultados dos testes, especialmente se eles forem executados em paralelo, deixando-os inconsistentes. Os testes de unidade normalmente podem ser executados em paralelo. Os testes de integração podem não dar suporte à execução paralela dependendo da implementação. Testar em relação ao banco de dados não é um teste de unidade, mas sim um teste de integração. É interessante ter muitos testes de unidade em execução rápida, mas menos testes de integração em relação aos bancos de dados.

Em termos de separação de interesses para os testes de unidade, a lógica opera em entidades de domínio na memória. Ela considera que a classe de repositório as entregou. Depois que a lógica modifica as entidades de domínio, ela considera que a classe de repositório as armazenará corretamente. O ponto importante aqui é criar testes de unidade em relação ao seu modelo de domínio e à sua lógica de domínio. As raízes de agregação são os limites de consistência principais em DDD.

Os repositórios implementados em eShopOnContainers contam com a implementação DbContext do EF Core dos padrões Repositório e Unidade de Trabalho usando o rastreador de alterações para que não dupliquem essa funcionalidade.

A diferença entre o padrão de repositório e o padrão da classe DAL (classe de acesso a dados) herdada

Um objeto DAL típico executa diretamente operações de persistência e acesso a dados no armazenamento, muitas vezes no nível de uma tabela e linha. As operações CRUD simples implementadas com um conjunto de classes DAL frequentemente não dão suporte a transações (embora isso nem sempre seja o caso). A maioria das abordagens de classe DAL faz uso mínimo de abstrações, o que resulta no acoplamento rígido entre classes de aplicativo ou BLL (camada lógica de negócios) que chamam os objetos DAL.

Ao usar o repositório, os detalhes de implementação da persistência são encapsulados fora do modelo de domínio. O uso de uma abstração facilita estender o comportamento por meio de padrões como Decoradores ou Proxies. Por exemplo, preocupações transversais, como cache, registro em log e tratamento de erro, podem ser aplicadas usando esses padrões em vez de padrões embutidos em código no próprio código de acesso a dados. Também é importante dar suporte a vários adaptadores de repositório que podem ser usados em ambientes diferentes, desde o desenvolvimento local até ambientes de preparo compartilhados até a produção.

Implementação da unidade de trabalho

Uma unidade de trabalho é uma transação que envolve várias operações de inserção, atualização ou exclusão. Simplificando, isso significa que, para uma ação de usuário específica, como o registro em um site, as operações de inserção, atualização e exclusão são tratadas em uma única transação. Ela é mais eficiente do que gerenciar várias transações de banco de dados de um modo mais extenso.

Essas várias operações de persistência serão executadas mais tarde em uma única ação quando o código da camada de aplicativo executar um comando para isso. A decisão de como aplicar as alterações realizadas na memória ao armazenamento de banco de dados real geralmente se baseia no padrão de unidade de trabalho. No EF, o padrão Unidade de Trabalho é implementado por um DbContext e é executado quando uma chamada é feita para SaveChanges.

Em muitos casos, esse padrão ou uma maneira de aplicar operações em relação ao armazenamento pode aumentar o desempenho do aplicativo e reduzir a possibilidade de inconsistências. Além disso, ele reduz o bloqueio de transações nas tabelas do banco de dados, porque todas as operações pretendidas são confirmadas em uma única transação. Isso é mais eficiente em comparação com a execução de muitas operações isoladas no banco de dados. Portanto, o ORM selecionado é capaz de otimizar a execução no banco de dados agrupando várias ações de atualização na mesma transação, em vez de executar várias transações pequenas e separadas.

O padrão Unidade de Trabalho pode ser implementado com ou sem usar o padrão Repositório.

Os repositórios não são obrigatórios

Os repositórios personalizados são úteis pelos motivos já citados e essa é a abordagem para o microsserviço de pedidos no eShopOnContainers. No entanto, esse não é um padrão essencial a ser implementado em um design DDD ou até mesmo no desenvolvimento para .NET, em geral.

Por exemplo, Jimmy Bogard, ao fornecer comentários diretos para este guia, diz o seguinte:

Provavelmente, este será o meu comentário mais importante. Eu realmente não sou fã de repositórios, principalmente porque eles ocultam detalhes importantes do mecanismo de persistência subjacente. É por isso que eu também uso o MediatR para comandos. Posso usar toda a capacidade da camada de persistência e enviar por push todo esse comportamento de domínio para minhas raízes de agregação. Geralmente, eu não gosto de usar repositórios fictícios, ou seja, eu ainda preciso aplicar esse teste de integração na situação real. Trabalhar com CQRS significa que realmente não é mais necessário o uso de repositórios.

Os repositórios podem ser úteis, mas eles não são críticos para o design DDD como o padrão Agregação e o modelo de domínio avançado. Portanto, use o padrão de repositório ou não, conforme achar mais adequado.

Recursos adicionais

Padrão de Repositório

Padrão de unidade de trabalho