.NET - Revisitando o Padrão Unit of Work
 Neste artigo vamos voltar a tratar do padrão Unit Of Work, o que é , como funciona , para que serve.

O padrão Unit of Work (IOW) ou Unidade de Trabalho é um conceito que esta vinculado à implementação do padrão repositório.

Padrão Repositório

Assim para entender o IOW temos que entender o conceito do padrão repositório. Vou apresentar o conceito sem entrar em detalhes para não desviar o foco do artigo.

Foi Martin Fowler quem definiu o padrão Repository no seu livro - Patterns of Enterprise Application Architecture - da seguinte forma: "Intermedeia entre o domínio e as camadas de mapeamento de dados usando uma interface de coleção para acessar objetos de domínio." (numa tradução livre by Macoratti)

Assim um repositório nada mais é do que uma classe definida para uma entidade, com todas as operações possíveis nessa entidade específica. Por exemplo, um repositório para uma entidade Cliente, terá operações CRUD básicas e quaisquer outras operações possíveis relacionadas a ele.

Podemos implementar o o padrão Repositório de duas maneiras :

  1. Repositório Específico (não genérico)  : Este tipo de implementação envolve o uso de uma classe de repositório para cada entidade. Por exemplo, se você tiver duas entidades Pedido e Cliente, cada entidade terá seu próprio repositório.
  2. Repositório Genérico: Um repositório genérico é aquele que pode ser usado para todas as entidades, ou seja, pode ser usado para Pedido ou Cliente ou qualquer outra entidade.

O padrão Unit Of Work ?

Unit Of Work ou Unidade de Trabalho é um padrão de projeto, e, de acordo com Martin Fowler, o padrão de unidade de trabalho "mantém uma lista de objetos afetados por uma transação e coordena a escrita de mudanças e trata possíveis problemas de concorrência".

O padrão Unit of Work esta presente em quase todas as ferramentas OR/M atuais (digo quase pois não conheço todas) e geralmente você não terá que criar a sua implementação personalizada a menos que decida realmente fazer isso por uma questão de força maior.

Então o padrão Unit of Work pode ser visto como um contexto, sessão ou objeto que acompanha as alterações das entidades de negócio durante uma transação sendo também responsável pelo gerenciamento dos problemas de concorrência que podem ocorrer oriundos dessa transação.

De uma forma mais simples e intuitiva significa que todas as transações (CRUD, etc.) são feitas em uma única transação ao invés de várias transações com o banco de dados. Assim uma unidade de trabalho envolve as operações em uma única transação.

Vamos agora mostrar na prática a aplicação destes conceitos.

Recursos usados :

Repositório para Cliente e Pedido

Vamos supor que temos um modelo de domínio contendo as entidades Cliente e Pedido e o seguinte arquivo de contexto chamado AppDbContext:

Para entender este conceito, considere a seguinte implementação do Padrão de Repositório usando um repositório não genérico, ClienteRepository para a entidade Cliente :

Neste exemplo estou implementando apenas os métodos Add() e GetAll() para simplificar o código.

Quando temos apenas um repositório as coisas são bem simples e provavelmente não teremos problemas.

Vamos agora implementar outro repositório chamado PedidoRepository para a entidade Pedido:

Agora temos dois repositórios que irão gerar e manter cada um sua própria instância do DbContext. Isso pode levar a problemas no futuro, uma vez que cada DbContext terá sua própria lista na memória de alterações dos registros, das entidades, que estão sendo adicionadas, atualizadas ou excluídas, em uma única transação.

Nesse caso, se o SaveChanges de um dos repositórios falhar e o do outro for bem-sucedido, isso resultará em inconsistências do banco de dados. É aqui que entra o padrão UnitOfWork.

Uma forma de contornar esse problema é incluir outra camada entre o repositório e controlador que vai funcionar como um armazenamento centralizado para todos os repositórios para receber a instância do DbContext.

Isso garantirá que, para uma unidade de transação, que se estende por vários repositórios, seja concluída para todas as entidades ou falhe totalmente, já que todas elas compartilharão a mesma instância do DbContext. Essa camada é o padrão Unit Of Work.

Assim, para o nosso exemplo,  ao adicionar dados para as entidades Pedido e Cliente, em uma única transação, ambos usarão a mesma instância DbContext.

Isso vai garantir que mesmo se um deles falhar, o outro também não será salvo, mantendo assim a consistência do banco de dados. Portanto, quando SaveChanges for executado, isso será feito para ambos os repositórios.

A seguir temos o código que implementa o padrão UnitOfWork.

1- Interface IUnitOfWork

using CShp_UOW1.Repositories;
namespace CShp_UOW1.Uow
{
    public interface IUnitOfWork
    {
        IClienteRepository ClienteRepo { get; }
        IPedidoRepository PedidoRepo { get; }
        void Commit();
    }
}

2- Classe UnitOfWork

using CShp_UOW1.Repositories;
namespace CShp_UOW1.Uow
{
    public class UnitOfWork : IUnitOfWork
    {
        public IClienteRepository _clienteRepository;
        public IPedidoRepository _pedidoRepository;
        public AppDbContext _context;

        public UnitOfWork(AppDbContext context)
        {
            _context = context;
        }
        public IClienteRepository ClienteRepo
        {
            get
            {
                if (_clienteRepository == null)
                {
                    _clienteRepository = new ClienteRepository(_context);
                }
                return _clienteRepository;
            }
        }
        public IPedidoRepository PedidoRepo
        {
            get
            {
                if (_pedidoRepository == null)
                {
                    _pedidoRepository = new PedidoRepository(_context);
                }
                return _pedidoRepository;
            }
        }

        public void Commit()
        {
            _context.SaveChanges();
        }
    }
}

Essa classe recebe a instância do DbContext e vai gerar as instâncias de cada repositório necessárias, em outras palavras, as instâncias de repositório para Pedido e Cliente passando o mesmo DbContext para ambos os repositórios.

Note que estamos criando propriedades que irão expor os repositórios concretos e também temos o método Commit() para ser usado após todas as modificações serem concluídas em um determinado objeto.

Esta é uma boa prática porque agora podemos, por exemplo, adicionar dois clientes, modificar dois pedidos e excluir um cliente, tudo em um método, e então chamar o método Commit() apenas uma vez.

Todas as alterações serão aplicadas ou se algo falhar, todas as alterações serão revertidas.

Supondo uma aplicação ASP .NET Core como exemplo, para usar o recurso basta registrar o serviço no método ConfigureServices() da classe Startup:

public static void ConfigureServices(this IServiceCollection services)
{
    services.AddScoped<IUnitOfWork, UnitOfWork>();
}

A seguir  basta fazer a injeção do contexto no construtor dos controladores ClienteController e PedidoController para poder usar a mesma instância do contexto (AppDbContext).

Agora tanto o repositório de cliente quanto o do pedido usam a mesma instância de DbContext e estamos executando o método SaveChanges usando a instância da classe UnitOfWork, assim, as alterações de uma única transação são feitas para ambos os repositórios ou para nenhum.

Dessa forma saímos de um cenário onde tínhamos várias instâncias do contexto sendo gerenciadas cada uma pelo seu repositório:

Para um cenário onde o padrão Unit Of Work gerencia o contexto e permite assim evitar problemas de concorrência e de inconsistências :

"E, se não há ressurreição de mortos, também Cristo não ressuscitou.
E, se Cristo não ressuscitou, logo é vã a nossa pregação, e também é vã a vossa fé."
1 Coríntios 15:13,14

Referências:


José Carlos Macoratti