Clean Architecture - Implementando o padrão Unit Of Work


 Hoje veremos como implementar o padrão Unit Of Work em um projeto ASP.NET Core Web API que usa a Clean Architecture.

A Clean Architecture (Arquitetura Limpa) é um conjunto de princípios e diretrizes para projetar e organizar sistemas de software de forma a promover a separação de preocupações, a modularidade, a testabilidade e a manutenção facilitada. Ela foi proposta por Robert C. Martin, também conhecido como Uncle Bob, e se baseia em princípios de design sólidos e na separação clara das diferentes camadas de um sistema.

Vamos mostrar a implementação do padrão em um projeto ASP.NET Core Web API que vai gerenciar informações sobre usuários definidos pela entidade User que vai conter as propriedades Email e Name. (Estamos simplificando o modelo de domínio)

Para aplicar a Arquitetura Limpa, podemos dividir a aplicação em quatro camadas:

A estrutura do projeto criado é mostrado a seguir:

Podemos identificar neste projeto os seguintes componentes:

  1. Uma solução em branco - Blank Solution - CleanArchitecture;
  2. 4 Solution Folders :  Core , Infrastructure, Presentation e Test;
  3. Core :  Contém dois projetos do tipo Class Library : CleanArchitecdture.Domain e CleanArchitecdture.Application
  4. Infrastructure : Contém um projeto do tipo Class Library : CleanArchitecdture.Persistence
  5. Presentation : Contém um projeto do tipo Asp.Net Core Web Api : CleanArchitecdture.WebAPI
  6. Test : Contém um projeto do tipo xUnit : CleanArchitecdture.UnitTest

Repositório

Um Repositório é usado para gerenciar a persistência e recuperação agregadas. Ele faz a mediação entre a camada de acesso a dados e o domínio e desacopla a camada de domínio da camada de dados de forma eficaz. Ele faz isso fornecendo acesso semelhante a uma coleção aos dados subjacentes.

O repositório oferece uma interface de coleção, fornecendo métodos para adicionar, modificar, remover e buscar objetos de domínio. Isso permite que o domínio permaneça agnóstico em relação ao mecanismo de persistência subjacente. Isso permite que ambas as camadas evoluam independentemente, mantendo alta coesão com baixo acoplamento.

Na figura abaixo temos uma comparação das abordagens sem usar repositório, usando o repositório e usando o repositório e o padrão unit of work:

Unidade de Trabalho (Unit of Work)

O padrão Unidade de Trabalho (UoW) é um padrão de projeto muito utilizado que ajuda a gerenciar transações e manter a consistência dos dados em aplicativos. Quando combinado com a Arquitetura Limpa, fornece uma base sólida para a construção de aplicativos escaláveis e de fácil manutenção.

Este padrão é usado para gerenciar transações e garantir que múltiplas operações sejam tratadas como uma única unidade lógica. Ele fornece uma maneira de agrupar operações de banco de dados, garantindo que elas sejam bem-sucedidas ou falhem como um todo. O princípio fundamental por trás do padrão Unidade de Trabalho é manter a consistência e a integridade dos dados, confirmando ou revertendo alterações de maneira coordenada.

A figura abaixo compara 3 abordagens mostrando o uso do repositório com o padrão Unit Of Work:

Integrando o Padrão de Unidade de Trabalho na Arquitetura Limpa

Para implementar o padrão Unidade de Trabalho em um aplicativo .NET Core no contexto da Arquitetura Limpa, podemos seguir as seguintes etapas:

1- Defina as interfaces principais

- Crie uma interface IUnitOfWork  que vai definir o contrato para a Unidade de Trabalho, incluindo métodos como Commit() ou Save(), Rollback() e outros métodos que sejam pertinentes ao cenário da sua aplicação;

2- Implemente a Unidade de Trabalho

Crie uma implementação concreta da interface IUnitOfWork que deverá encapsulará os limites transacionais e coordenará as operações do banco de dados. A implementação da Unidade de Trabalho deve fornecer métodos para iniciar uma transação, confirmar alterações e reverter a transação, se necessário.

3- Fazer a integração com repositórios

Modifique as implementações dos seus repositórios para funcionarem com a implementação da  Unidade de Trabalho. Cada método de repositório deve ser incluído na transação da Unidade de Trabalho, garantindo que as alterações feitas pelo repositório sejam confirmadas ou revertidas juntas.

4- Integração da camada de aplicação

Na camada de Aplicativo, injete a interface IUnitOfWork no comando apropriado ou nos manipuladores de consulta que exigem operações transacionais. Dentro desses manipuladores, execute as operações necessárias usando os repositórios agrupados na Unidade de Trabalho.

5- Usando a Unit of Work implementada

Quando um comando ou consulta requer operações transacionais, recupere a instância da Unidade de Trabalho por meio de injeção de dependência. Comece a transação usando o método da Unidade de Trabalho. Execute as operações de banco de dados necessárias usando os repositórios agrupados na Unidade de Trabalho. Por fim, chame o método Commit() da Unidade de Trabalho para persistir as alterações ou o método Rollback() para descartar as alterações.

Da teoria para a prática

De forma a dar uma visão mais concreta sobre as etapas acima descritas vamos considerar um exemplo de uma aplicação de vendas de produtos para ilustrar o uso do padrão Unit Of Work.

1- Definindo a interface

public interface IUnitOfWork : IDisposable
{
    void Commit();
    void Rollback();
    IRepository<TEntity> GetRepository<TEntity>() where TEntity : class;
}

2- Implementando a interface

public class UnitOfWork : IUnitOfWork
{
    private readonly DbContext _context;
    private Dictionary<Type, object> _repositories;

    public UnitOfWork(DbContext context)
    {
        _context = context;
        _repositories = new Dictionary<Type, object>();
    }

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

    public void Rollback()
    {
        // Rollback se for necessário
    }

    public IRepository<TEntity> GetRepository<TEntity>() where TEntity : class
    {
        if (_repositories.ContainsKey(typeof(TEntity)))
        {
            return (IRepository<TEntity>)_repositories[typeof(TEntity)];
        }

        var repository = new Repository<TEntity>(_context);
        _repositories.Add(typeof(TEntity), repository);
        return repository;
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Vamos entender este código:

Iniciamos definindo as seguintes variáveis :

A seguir no construtor temos que :

  1. Método Commit():
  2. Método Rollback():
  3. Método GetRepository<TEntity>():
  4. Método Dispose():

3- Integrando com os repositórios

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private readonly DbContext _context;
    private readonly DbSet<TEntity> _dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        _dbSet = _context.Set<TEntity>();
    }

    // implementação dos métodos dos repositórios
}

4- Usando a implementação na camada Application

public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IRepository<Product> _productRepository;

    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _productRepository = _unitOfWork.GetRepository<Product>();
    }

    public void CreateProduct(Product product)
    {
        // Executa a lógica de negócio e as operações do repositório usando _productRepository...

        _unitOfWork.Commit();
    }
}

Com este exemplo mostramos como o padrão Unidade de Trabalho pode ser integrado a uma aplicação de vendas para lidar com operações de banco de dados. Com este padrão, podemos garantir que as operações relacionadas sejam tratadas como uma única unidade lógica, promovendo a integridade dos dados e a consistência transacional.

E estamos conversados...

"E disse-lhe Jesus: Em verdade te digo que hoje estarás comigo no Paraísoadm."
Lucas 23:43

Referências:


José Carlos Macoratti