André Alves de Lima

Talking about Software Development and more…

Desacoplando o acesso a dados da aplicação com o padrão Repository e Unit of Work

Os padrões Repository e Unit of Work são design patterns muito conhecidos e a cada dia mais utilizados no desenvolvimento de aplicações de negócios. O grande problema é que a maioria dos exemplos que encontramos desses padrões é voltada para desenvolvimento web, ou utiliza uma tecnologia muito específica na implementação (por exemplo, Repository para Entity Framework). O objetivo do artigo de hoje é explicar os conceitos do padrão Repository e Unit of Work, listar as vantagens e desvantagens da sua utilização, bem como implementar um exemplo desses padrões em uma Console Application.

Disclaimer

Eu sempre gosto de deixar bem claro nos meus artigos e vídeos quando eu estou falando de algo que eu não utilizo no dia a dia. Se você já consumiu algum conteúdo meu que não tenha uma seção de “disclaimer“, isso significa que eu já coloquei em prática o assunto que foi abordado e que eu relativamente domino aquela área. Esse não é o caso do padrão Repository e Unit of Work, uma vez que eu nunca os utilizei em produção.

Até o momento em que eu parei para escrever esse artigo, eu nunca tinha nem parado para implementar um exemplo utilizando esses padrões! Eu já sabia exatamente o conceito por trás deles e as suas vantagens. Além disso, eu já tinha consumido inúmeros conteúdos de outras pessoas onde esses padrões tinham sido utilizados.

Dito isso, tudo o que eu vou mostrar no artigo de hoje foi o resultado do meu aprendizado nos últimos dias ao preparar um exemplo de Console Application utilizando os padrões Repository e Unit of Work. Essa Console Application será totalmente desacoplada da camada de acesso a dados concreta, possibilitando o armazenamento de dados tanto em memória quanto em um banco de dados (utilizando Entity Framework Code First).

Se você quiser conferir outros conteúdos de pessoas que aparentemente já têm mais experiência com esses padrões, eu recomendo que você dê uma olhada nos seguintes links (que me ajudaram como base na construção deste artigo):

Creating a Data Repository using Dapper (indicação do leitor Fausto Luis)
Ebook do Fabio Silva Lima sobre Design Patterns vol.1
Repository Pattern with C# and Entity Framework, Done Right
Quando usar Entity Framework com Repository Pattern?
Entity Framework Repositório Genérico

O que é o padrão Repository?

Como mencionei anteriormente, o padrão Repository é um design pattern que pode ajudar muito quando implementamos o acesso a dados das nossas aplicações. Ele abstrai toda a parte de armazenamento de dados e consultas ao banco, servindo como um mediador entre a aplicação e a camada de mapeamento de objetos.

Quando utilizamos o padrão Repository, todas as nossas consultas customizadas são implementadas em um único local, evitando a repetição de código. Por exemplo, imagine que em uma determinada aplicação nós precisemos retornar todos os produtos de uma determinada categoria. Onde é que nós implementaríamos esse tipo de código? Direto na aplicação? E se nós precisarmos utilizar essa mesma consulta mais de uma vez? Como fazemos para não repetirmos o mesmo código em locais diferentes da aplicação?

Uma alternativa seria trabalharmos com uma camada de Data Access Objects, onde implementaríamos tanto os códigos do CRUD (criação, alteração, exclusão e listagem) quando as consultas customizadas. Outra opção seria trabalharmos com o padrão Repository, que é justamente o que eu vou mostrar para você no artigo de hoje.

O padrão Repository se resume basicamente a uma interface genérica, chamada de IRepository. Essa interface definirá o contrato dos métodos básicos de inclusão (Add), remoção (Remove) e listagem de registros (GetAll) que todo repositório concreto deverá implementar:

Nota: os nomes para os métodos podem variar dependendo da implementação. Algumas pessoas optam por “Create” ao invés de “Add” e “Delete” ao invés de “Remove”. Eu escolhi esses nomes para manter o mesmo padrão das implementações de List (do .NET) e DbSet (do Entity Framework).

Uma vez estabelecido esse padrão, para cada entidade da nossa aplicação, nós teremos que criar um repositório concreto. Ou seja, se tivermos uma entidade “Produto“, nós teremos que ter um repositório de produtos (ProdutoRepository). Se tivermos uma entidade “Cliente“, precisaremos de um repositório para ele também (ClienteRepository), e assim por diante.

Dentro dos repositórios específicos, além dos métodos básicos de inclusão, remoção e listagem de registros, nós teremos os métodos de consultas customizadas. No exemplo que dei anteriormente do método que retornaria todos os produtos de uma determinada categoria, esse método seria definido na interface de repositório específica do produto (IProdutoRepository).

E o Unit of Work?

Note que na explicação acima sobre o padrão Repository, eu não falei nada sobre a parte de “salvar” as entidades. A responsabilidade do repositório é simplesmente armazenar uma coleção de entidades em memória. A persistência das informações não é de responsabilidade do repositório, mesmo porque, como conseguiríamos controlar transações feitas em mais de um repositório se a responsabilidade da persistência estivesse nesse elemento? Por exemplo, como faríamos para transacionar a operação de venda e baixa de estoque se a responsabilidade de salvar as informações estivesse separada nos repositórios de venda e estoque?

Para resolvermos esse problema, temos outro design pattern chamado Unit of Work. Esse padrão se resume a uma interface (IUnitOfWork) que terá todos os repositórios da aplicação, além de um método que será responsável por salvar as alterações (Save).

Na aplicação que iremos construir neste artigo, nós teremos dois repositórios. Um repositório conterá os produtos e outro repositório conterá as categorias de produtos. Dessa forma, a interface IUnitOfWork nesse projeto de exemplo terá os dois repositórios e o método “Save“:

Não se preocupe se as coisas não estiverem claras até aqui. Eu também demorei um pouco para entender completamente esses dois padrões. Daqui a pouco nós veremos um exemplo concreto e tudo ficará mais claro.

Vantagens ao utilizar esses padrões

A primeira vantagem que temos ao utilizar os padrões Repository e Unit of Work é que, como mencionei anteriormente, as consultas complexas ficam todas armazenadas em um único local. Além disso, diferentemente dos DAOs, com o padrão Repository fica mais fácil de implementarmos um controle de acesso misto (onde, por exemplo, consultas seriam feitas com Dapper e o resto ficaria a cargo de algum ORM mais robusto).

Além disso, ao implementarmos esses padrões, nós podemos facilmente substituir de forma completa a camada de persistência de dados da nossa aplicação. Ou seja, nós poderíamos trocar o armazenamento de dados de um ORM para outro (do Entity Framework para o NHibernate, por exemplo) sem que o código central da aplicação precise ser modificado. Resumindo, a aplicação não tem a mínima ideia de onde os dados estão sendo armazenados, ela só conhece os repositórios genéricos.

Por fim, outra grande vantagem que temos ao utilizarmos repositórios é na parte de “testabilidade“. Com o padrão Repository e Unit of Work, fica muito mais fácil criarmos “mocks” para testarmos as regras de negócio da nossa aplicação de forma independente do banco de dados.

Quando não devemos utiliza-los?

Esses padrões, apesar de serem muito úteis, trazem uma certa complexidade adicional ao nosso código. Se o domínio da sua aplicação não for complexo, não faz sentido adicionar toda essa parafernália.

Além disso, se você tiver certeza absoluta que você não precisará trocar a camada de persistência da sua aplicação, você pode continuar com os seus DAOs mesmo, uma vez que, com eles, você conseguirá atingir basicamente o mesmo benefício desses padrões.

Padrão Repository e Unit of Work genéricos na prática

Eu sei que tudo o que eu falei até agora está muito abstrato, mas acho que a partir desse momento as coisas começarão a ficar mais claras. Vamos implementar na prática o padrão Repository e Unit of Work em uma aplicação de exemplo que conterá duas entidades: Produtos e Categorias de Produtos. Tudo isso será feito em uma aplicação console e, como resultado final nós conseguiremos, com uma linha de código, trocar completamente a estratégia de armazenamento de dados da aplicação (em memória / no banco de dados em Entity Framework).

Para não nos confundirmos, nós iremos separar a implementação em projetos diferentes. Um projeto será a Console Application e, inicialmente, teremos um outro projeto que conterá as entidades e repositórios genéricos.

Então vamos começar criando um projeto do tipo Console Application e, na mesma solução, vamos adicionar um novo projeto do tipo “Class Library“. Dentro desse projeto “Class Library“, nós vamos adicionar as nossas duas entidades (CategoriaProduto e Produto):

    // C#
    public class CategoriaProduto
    {
        public int Id { get; set; }
        public string Nome { get; set; }
    }
    // C#
    public class Produto
    {
        public int Id { get; set; }
        public string Descricao { get; set; }
        public CategoriaProduto Categoria { get; set; }
        public int CategoriaId { get; set; }
        public DateTime DataCadastro { get; set; }
        public decimal ValorUnitario { get; set; }
    }
' VB.NET
Public Class CategoriaProduto
    Public Property Id As Integer
    Public Property Nome As String
End Class
' VB.NET
Public Class Produto
    Public Property Id As Integer
    Public Property Descricao As String
    Public Property Categoria As CategoriaProduto
    Public Property CategoriaId As Integer
    Public Property DataCadastro As DateTime
    Public Property ValorUnitario As Decimal
End Class

Em seguida, vamos adicionar uma nova pasta (chamada “Repository“) no projeto “Class Library“, onde colocaremos os repositórios genéricos da nossa aplicação. Dentro dessa pasta, vamos adicionar a nossa interface “IRepository“, que terá a estrutura que mencionei anteriormente, com os métodos “GetAll“, “Add” e “Remove“:

    // C#
    public interface IRepository<T> where T : class
    {
        IEnumerable<T> GetAll();
        void Add(T entity);
        void Remove(T entity);
    }
' VB.NET
Namespace Repository
    Public Interface IRepository(Of T As Class)
        Function GetAll() As IEnumerable(Of T)
        Sub Add(ByVal entity As T)
        Sub Remove(ByVal entity As T)
    End Interface
End Namespace

Nota: se você não sabe o que é esse “T” na definição da interface, ele está basicamente indicando que a interface é genérica, ou seja, ela é ligada a algum tipo. Nesse caso, os repositórios serão de um tipo específico (qualquer classe, como definido na cláusula “where”). No nosso caso, nós teremos repositório de produtos e categorias de produtos, como veremos logo a seguir. É a mesma ideia de listas genérica de algum tipo (“List<string>”, por exemplo). Para mais informações sobre esse assunto, pesquise por “generics” no .NET.

Agora que já temos a nossa interface de repositório genérica, chegou a hora de definirmos as interfaces de repositório da nossa aplicação. No nosso caso, nós teremos uma interface de repositório para as categorias de produtos (ICategoriaProdutoRepository) e outra interface de repositório para os produtos (IProdutoRepository).

Na interface de repositório para as categorias de droduto, nós não teremos nenhum código customizado. Porém, na interface de repositório para os produtos, nós definiremos o contrato para o método que deverá retornar os produtos de uma determinada categoria (GetProdutosPorCategoria):

    // C#
    public interface ICategoriaProdutoRepository : IRepository<CategoriaProduto>
    {
    }
    // C#
    public interface IProdutoRepository : IRepository<Produto>
    {
        IEnumerable<Produto> GetProdutosPorCategoria(CategoriaProduto categoria);
    }
' VB.NET
Namespace Repository
    Public Interface ICategoriaProdutoRepository
        Inherits IRepository(Of CategoriaProduto)
    End Interface
End Namespace
' VB.NET
Namespace Repository
    Public Interface IProdutoRepository
        Inherits IRepository(Of Produto)
        Function GetProdutosPorCategoria(ByVal categoria As CategoriaProduto) As IEnumerable(Of Produto)
    End Interface
End Namespace

Por fim, só está faltando a definição do nosso Unit of Work. No nosso caso, dentro da nossa interface IUnitOfWork nós teremos uma instância de cada interface de repositório, além do método “Save“:

    // C#
    public interface IUnitOfWork
    {
        ICategoriaProdutoRepository CategoriaProduto { get; }
        IProdutoRepository Produto { get; }
        void Save();
    }
' VB.NET
Namespace Repository
    Public Interface IUnitOfWork
        ReadOnly Property CategoriaProduto As ICategoriaProdutoRepository
        ReadOnly Property Produto As IProdutoRepository
        Sub Save()
    End Interface
End Namespace

E com isso nós finalizamos a implementação do nosso Repository e Unit of Work genéricos. Agora nós podemos desenvolver uma aplicação que só conhecerá essas interfaces, de forma que ela não tenha nenhuma dependência com uma implementação concreta. Vamos fazer isso no nosso projeto Console Application, que deverá ter uma referência para o projeto “Class Library“, onde implementamos as entidades e o repositório genérico.

Dentro da classe “Program” nós definiremos uma instância da nossa interface “IUnitOfWork“. Em seguida, dentro do método “main“, nós faremos um workflow de listagem, criação, alteração e exclusão de produtos:

        // C#
        private static Model.Repository.IUnitOfWork contexto;

        static void Main(string[] args)
        {
            contexto = null;

            List();

            Create();
            List();

            Update();
            List();

            Delete();
            List();

            System.Console.ReadLine();
        }
    ' VB.NET
    Private Contexto As Model.VB.Repository.IUnitOfWork

    Public Sub Main()
        Contexto = Nothing

        List()
        Create()

        List()
        Update()

        List()
        Delete()

        List()
        System.Console.ReadLine()
    End Sub

Nota: por enquanto nós setamos o valor do nosso contexto para “nulo”, uma vez que nós ainda não temos nenhuma implementação concreta do repositório. Obviamente esse código não funcionará até o momento em que nós substituirmos essa declaração por uma implementação concreta do repositório.

Veja só o código dos métodos “List“, “Create“, “Update” e “Delete“:

        // C#
        private static void List()
        {
            System.Console.WriteLine("======= LISTAGEM DE PRODUTOS =======");
            foreach (var produto in contexto.Produto.GetAll())
            {
                System.Console.WriteLine("Id: {0}, Descricao: {1}, Categoria: {2}, Data Cadastro: {3:d}, Valor Unitário: {4:n2}",
                    produto.Id, produto.Descricao, produto.Categoria.Nome, produto.DataCadastro, produto.ValorUnitario);
            }
            System.Console.WriteLine("====================================");
        }
        private static void Create()
        {
            var novoProduto = new Model.Produto();
            novoProduto.Descricao = "Novo produto";
            novoProduto.Categoria = contexto.CategoriaProduto.GetAll().First();
            novoProduto.DataCadastro = DateTime.Now;
            novoProduto.ValorUnitario = 2.34m;
            contexto.Produto.Add(novoProduto);
            contexto.Save();

            System.Console.WriteLine("====================================");
            System.Console.WriteLine("PRODUTO CADASTRADO COM SUCESSO");
            System.Console.WriteLine("====================================");
        }
        private static void Update()
        {
            var produto = contexto.Produto.GetAll().Last();
            produto.Descricao = "Produto alterado";
            contexto.Save();

            System.Console.WriteLine("====================================");
            System.Console.WriteLine("PRODUTO ALTERADO COM SUCESSO");
            System.Console.WriteLine("====================================");
        }
        private static void Delete()
        {
            var produto = contexto.Produto.GetAll().Last();
            contexto.Produto.Remove(produto);
            contexto.Save();

            System.Console.WriteLine("====================================");
            System.Console.WriteLine("PRODUTO EXCLUÍDO COM SUCESSO");
            System.Console.WriteLine("====================================");
        }
    ' VB.NET
    Private Sub List()
        System.Console.WriteLine("======= LISTAGEM DE PRODUTOS =======")
        For Each Produto In Contexto.Produto.GetAll()
            System.Console.WriteLine("Id: {0}, Descricao: {1}, Categoria: {2}, Data Cadastro: {3:d}, Valor Unitário: {4:n2}", Produto.Id, Produto.Descricao, Produto.Categoria.Nome, Produto.DataCadastro, Produto.ValorUnitario)
        Next

        System.Console.WriteLine("====================================")
    End Sub

    Private Sub Create()
        Dim NovoProduto = New Model.VB.Produto()
        NovoProduto.Descricao = "Novo produto"
        NovoProduto.Categoria = Contexto.CategoriaProduto.GetAll().First()
        NovoProduto.DataCadastro = DateTime.Now
        NovoProduto.ValorUnitario = 2.34D
        Contexto.Produto.Add(NovoProduto)
        Contexto.Save()
        System.Console.WriteLine("====================================")
        System.Console.WriteLine("PRODUTO CADASTRADO COM SUCESSO")
        System.Console.WriteLine("====================================")
    End Sub

    Private Sub Update()
        Dim Produto = Contexto.Produto.GetAll().Last()
        Produto.Descricao = "Produto alterado"
        Contexto.Save()
        System.Console.WriteLine("====================================")
        System.Console.WriteLine("PRODUTO ALTERADO COM SUCESSO")
        System.Console.WriteLine("====================================")
    End Sub

    Private Sub Delete()
        Dim Produto = Contexto.Produto.GetAll().Last()
        Contexto.Produto.Remove(Produto)
        Contexto.Save()
        System.Console.WriteLine("====================================")
        System.Console.WriteLine("PRODUTO EXCLUÍDO COM SUCESSO")
        System.Console.WriteLine("====================================")
    End Sub

Observe que a aplicação não tem a mínima ideia de onde os dados serão armazenados. Ela só conhece as interfaces genéricas e não sabe de nenhuma implementação concreta do repositório.

Repository em memória

Agora que nós já temos a implementação da nossa aplicação, chegou a hora de criarmos um repositório concreto. O primeiro repositório concreto que criaremos armazenará os dados em memória, dentro de listas genéricas. Para não misturarmos as coisas, vamos criar um novo projeto “Class Library” onde faremos essa implementação. Esse novo projeto deverá referenciar o outro projeto onde as entidades foram definidas.

Dentro desse projeto, vamos adicionar uma classe chamada “Repository“. Essa será a classe que servirá de base para todos os outros repositórios concretos. Vamos ver como é que fica o código dessa classe:

    // C#
    public abstract class Repository<T> : Model.Repository.IRepository<T> where T : class
    {
        protected List<T> _lista = new List<T>();

        public virtual void Add(T entity)
        {
            _lista.Add(entity);
        }

        public virtual IEnumerable<T> GetAll()
        {
            return _lista.AsEnumerable();
        }

        public virtual void Remove(T entity)
        {
            _lista.Remove(entity);
        }
    }
' VB.NET
Public MustInherit Class Repository(Of T As Class)
    Implements Model.VB.Repository.IRepository(Of T)

    Protected Lista As List(Of T) = New List(Of T)

    Public Overridable Sub Add(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Add
        Me.Lista.Add(Entity)
    End Sub

    Public Overridable Function GetAll() As IEnumerable(Of T) Implements Model.VB.Repository.IRepository(Of T).GetAll
        Return Me.Lista.AsEnumerable
    End Function

    Public Overridable Sub Remove(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Remove
        Me.Lista.Remove(Entity)
    End Sub
End Class

Observe que essa classe implementa a nossa interface genérica (IRepository), utilizando uma lista por trás dos panos, onde os dados serão armazenados.

Em seguida, vamos adicionar o nosso repositório concreto para as categorias dos produtos. Esse repositório não terá nenhum código customizado, ele será somente uma implementação concreta de “Repository<CategoriaProduto>“:

    // C#
    public class CategoriaProdutoRepository : Repository<Model.CategoriaProduto>, Model.Repository.ICategoriaProdutoRepository
    {
    }
' VB.NET
Public Class CategoriaProdutoRepository
    Inherits Repository(Of Model.VB.CategoriaProduto)
    Implements Model.VB.Repository.ICategoriaProdutoRepository
End Class

Por outro lado, o repositório concreto para os produtos deverá implementar duas lógicas adicionais. Uma delas é o método “GetProdutosPorCategoria“, que foi definido no contrato da interface “IProdutoRepository“. O outro código customizado é o cálculo do auto-incremento para o “Id” do Produto. Como nós não estamos salvando essas informações no banco de dados, nós temos que calcular o auto-incremento manualmente:

    // C#
    public class ProdutoRepository : Repository<Model.Produto>, Model.Repository.IProdutoRepository
    {
        public override void Add(Model.Produto entity)
        {
            if (entity.Id == 0)
            {
                entity.Id = GetMaxId() + 1;
            }
            base.Add(entity);
        }
        public IEnumerable<Model.Produto> GetProdutosPorCategoria(Model.CategoriaProduto categoria)
        {
            return _lista.Where(p => p.Categoria == categoria).AsEnumerable();
        }
        private int GetMaxId()
        {
            var maxId = 0;

            if (_lista.Any())
            {
                maxId = _lista.Max(p => p.Id);
            }

            return maxId;
        }
    }
' VB.NET
Public Class ProdutoRepository
    Inherits Repository(Of Model.VB.Produto)
    Implements Model.VB.Repository.IProdutoRepository

    Public Overrides Sub Add(ByVal Entity As Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository.Add
        If (Entity.Id = 0) Then
            Entity.Id = (Me.GetMaxId + 1)
        End If

        MyBase.Add(Entity)
    End Sub

    Public Function GetProdutosPorCategoria(ByVal Categoria As Model.VB.CategoriaProduto) As IEnumerable(Of Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository.GetProdutosPorCategoria
        Return Lista.Where(Function(p) p.Categoria Is Categoria).AsEnumerable
    End Function

    Private Function GetMaxId() As Integer
        Dim MaxId = 0
        If Lista.Any Then
            MaxId = Lista.Max(Function(p) p.Id)
        End If

        Return MaxId
    End Function
End Class

Por fim, a única coisa que está faltando é implementarmos a interface IUnitOfWork. Para isso, vamos adicionar uma nova classe no projeto, dando o nome de “UnitOfWork“. Essa classe herdará da interface “IUnitOfWork” e terá instâncias dos repositórios concretos que acabamos de criar. Além disso, no construtor nós adicionaremos três categorias padrão na tabela de categorias de produtos:

    // C#
    public class UnitOfWork : Model.Repository.IUnitOfWork
    {
        private CategoriaProdutoRepository _categoriaProduto = new CategoriaProdutoRepository();
        public Model.Repository.ICategoriaProdutoRepository CategoriaProduto { get { return _categoriaProduto; } }
        private ProdutoRepository _produto = new ProdutoRepository();
        public Model.Repository.IProdutoRepository Produto { get { return _produto; } }

        public UnitOfWork()
        {
            _categoriaProduto.Add(new Model.CategoriaProduto() { Id = 1, Nome = "Categoria 1" });
            _categoriaProduto.Add(new Model.CategoriaProduto() { Id = 2, Nome = "Categoria 2" });
            _categoriaProduto.Add(new Model.CategoriaProduto() { Id = 3, Nome = "Categoria 3" });
        }

        public void Save()
        {
            
        }
    }
' VB.NET
Public Class UnitOfWork
    Implements Model.VB.Repository.IUnitOfWork

    Private _categoriaProduto As CategoriaProdutoRepository = New CategoriaProdutoRepository
    Public ReadOnly Property CategoriaProduto As Model.VB.Repository.ICategoriaProdutoRepository Implements Model.VB.Repository.IUnitOfWork.CategoriaProduto
        Get
            Return Me._categoriaProduto
        End Get
    End Property

    Private _produto As ProdutoRepository = New ProdutoRepository
    Public ReadOnly Property Produto As Model.VB.Repository.IProdutoRepository Implements Model.VB.Repository.IUnitOfWork.Produto
        Get
            Return Me._produto
        End Get
    End Property

    Public Sub New()
        MyBase.New
        Me._categoriaProduto.Add(New Model.VB.CategoriaProduto)
        Me._categoriaProduto.Add(New Model.VB.CategoriaProduto)
        Me._categoriaProduto.Add(New Model.VB.CategoriaProduto)
    End Sub

    Public Sub Save() Implements Model.VB.Repository.IUnitOfWork.Save

    End Sub
End Class

Nota: o método “Save” do UnitOfWork em memória não fará nada, uma vez que os dados não serão persistidos em lugar nenhum.

Agora que já temos a implementação concreta do repositório em memória, vamos voltar ao nosso projeto Console Application, onde faremos uso dessa implementação. Primeiramente, temos que adicionar uma referência à nova “Class Library” onde a implementação do repositório em memória foi realizada. Em seguida, basta alterarmos a declaração do “IUnitOfWork” para que a nossa classe “UnitOfWork” seja utilizada:

            // C#
            contexto = new InMemory.UnitOfWork();
' VB.NET
Contexto = New InMemory.VB.UnitOfWork()

Execute a aplicação e veja o resultado:

Repository com Entity Framework

OK, a nossa aplicação está funcionando perfeitamente com o armazenamento dos dados em memória. Mas, e se agora nós quisermos armazenar as informações em um banco utilizando Entity Framework? É aí que nós começaremos a enxergar as vantagens do padrão Repository e Unit of Work. Basta nós implementarmos o repositório com Entity Framework, trocar a declaração para que o novo UnitOfWork concreto seja utilizado e pronto! A aplicação nem saberá que os dados estão sendo armazenados no banco ao invés de em memória.

Vamos começar criando um novo projeto do tipo “Class Library” e adicionando a referência para o Entity Framework através do NuGet. Se ainda não conhece nada sobre o Entity Framework, dê uma olhada primeiro no meu artigo introdutório sobre o Entity Framework. Ele é leitura obrigatória se você ainda não conhece os conceitos básicos do Entity Framework.

Uma vez criado o projeto e adicionada a referência para o Entity Framework, vamos adicionar uma nova classe chamada “Contexto“, que será o nosso contexto do Entity Framework:

    // C#
    internal class Contexto : System.Data.Entity.DbContext
    {
        public System.Data.Entity.DbSet<Model.CategoriaProduto> CategoriaProduto { get; set; }
        public System.Data.Entity.DbSet<Model.Produto> Produto { get; set; }

        public Contexto() : base()
        {
            if (!CategoriaProduto.Any())
            {
                CategoriaProduto.Add(new Model.CategoriaProduto() { Id = 1, Nome = "Categoria 1" });
                CategoriaProduto.Add(new Model.CategoriaProduto() { Id = 2, Nome = "Categoria 2" });
                CategoriaProduto.Add(new Model.CategoriaProduto() { Id = 3, Nome = "Categoria 3" });
                SaveChanges();
            }
        }

        protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Conventions.Remove<System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention>();
        }
    }
' VB.NET
Class Contexto
    Inherits System.Data.Entity.DbContext

    Public Property CategoriaProduto As System.Data.Entity.DbSet(Of Model.VB.CategoriaProduto)
    Public Property Produto As System.Data.Entity.DbSet(Of Model.VB.Produto)

    Public Sub New()
        MyBase.New
        If Not Me.CategoriaProduto.Any Then
            Me.CategoriaProduto.Add(New Model.VB.CategoriaProduto)
            Me.CategoriaProduto.Add(New Model.VB.CategoriaProduto)
            Me.CategoriaProduto.Add(New Model.VB.CategoriaProduto)
            SaveChanges()
        End If

    End Sub

    Protected Overrides Sub OnModelCreating(ByVal ModelBuilder As System.Data.Entity.DbModelBuilder)
        MyBase.OnModelCreating(ModelBuilder)
        ModelBuilder.Conventions.Remove(Of System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention)()
    End Sub
End Class

Como você pode observar, a única coisa que estamos fazendo de especial no contexto é a criação das categorias padrão (caso a tabela de categorias esteja vazia) e a desativação da pluralização das tabelas.

Com o contexto em mãos, chegou a hora de criarmos os nossos repositórios. Primeiramente, vamos adicionar uma classe chamada “Repository“, que servirá como base para todos os repositórios concretos. Veja como fica a implementação dessa classe:

    // C#
    public abstract class Repository<T> : Model.Repository.IRepository<T> where T : class
    {
        protected System.Data.Entity.DbSet<T> _dbSet;

        public Repository(System.Data.Entity.DbSet<T> dbSet)
        {
            _dbSet = dbSet;
        }

        public virtual void Add(T entity)
        {
            _dbSet.Add(entity);
        }

        public virtual IEnumerable<T> GetAll()
        {
            return _dbSet.AsEnumerable();
        }

        public virtual void Remove(T entity)
        {
            _dbSet.Remove(entity);
        }
    }
' VB.NET
Public MustInherit Class Repository(Of T As Class)
    Implements Model.VB.Repository.IRepository(Of T)

    Protected DbSet As System.Data.Entity.DbSet(Of T)

    Public Sub New(ByVal dbSet As System.Data.Entity.DbSet(Of T))
        MyBase.New
        Me.DbSet = dbSet
    End Sub

    Public Overridable Sub Add(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Add
        Me.DbSet.Add(Entity)
    End Sub

    Public Overridable Function GetAll() As IEnumerable(Of T) Implements Model.VB.Repository.IRepository(Of T).GetAll
        Return Me.DbSet.AsEnumerable
    End Function

    Public Overridable Sub Remove(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Remove
        Me.DbSet.Remove(Entity)
    End Sub
End Class

Note que a implementação desse repositório com o Entity Framework é muito parecida com a implementação em memória. A principal diferença é que nós estamos trabalhando por trás dos panos com um DbSet do Entity Framework (ao invés de um List em memória). O DbSet deverá ser passado por parâmetro no construtor.

Em seguida, vamos partir para a implementação dos repositórios CategoriaProdutoRepository e ProdutoRepository:

    // C#
    public class CategoriaProdutoRepository : Repository<Model.CategoriaProduto>, Model.Repository.ICategoriaProdutoRepository
    {
        public CategoriaProdutoRepository(System.Data.Entity.DbSet<Model.CategoriaProduto> dbSet) : base(dbSet) { }
    }
    // C#
    public class ProdutoRepository : Repository<Model.Produto>, Model.Repository.IProdutoRepository
    {
        public ProdutoRepository(System.Data.Entity.DbSet<Model.Produto> dbSet) : base(dbSet) { }

        public IEnumerable<Model.Produto> GetProdutosPorCategoria(Model.CategoriaProduto categoria)
        {
            return _dbSet.Where(p => p.Categoria == categoria).AsEnumerable();
        }
    }
' VB.NET
Public Class CategoriaProdutoRepository
    Inherits Repository(Of Model.VB.CategoriaProduto)
    Implements Model.VB.Repository.ICategoriaProdutoRepository

    Public Sub New(ByVal DbSet As System.Data.Entity.DbSet(Of Model.VB.CategoriaProduto))
        MyBase.New(DbSet)
    End Sub
End Class
' VB.NET
Public Class ProdutoRepository
    Inherits Repository(Of Model.VB.Produto)
    Implements Model.VB.Repository.IProdutoRepository

    Public Sub New(ByVal DbSet As System.Data.Entity.DbSet(Of Model.VB.Produto))
        MyBase.New(DbSet)

    End Sub

    Public Function GetProdutosPorCategoria(ByVal Categoria As Model.VB.CategoriaProduto) As IEnumerable(Of Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository.GetProdutosPorCategoria
        Return DbSet.Where(Function(p) p.Categoria Is Categoria).AsEnumerable
    End Function
End Class

Observe que dessa vez a implementação do repositório de produtos não precisa se preocupar com o cálculo do próximo “Id” do produto (como tivemos que fazer com o repositório em memória), uma vez que isso fica a cargo do Entity Framework.

Por fim, só está faltando a implementação da classe UnitOfWork. Veja como é que fica o código dela:

    // C#
    public class UnitOfWork : Model.Repository.IUnitOfWork
    {
        private Contexto _contexto;

        private Model.Repository.ICategoriaProdutoRepository _categoriaProduto;
        public Model.Repository.ICategoriaProdutoRepository CategoriaProduto
        {
            get
            {
                return _categoriaProduto;
            }
        }

        private Model.Repository.IProdutoRepository _produto;
        public Model.Repository.IProdutoRepository Produto
        {
            get
            {
                return _produto;
            }
        }

        public UnitOfWork()
        {
            _contexto = new Contexto();
            _categoriaProduto = new CategoriaProdutoRepository(_contexto.CategoriaProduto);
            _produto = new ProdutoRepository(_contexto.Produto);
        }

        public void Save()
        {
            _contexto.SaveChanges();
        }
    }
' VB.NET
Public Class UnitOfWork
    Implements Model.VB.Repository.IUnitOfWork

    Private Contexto As Contexto

    Private _categoriaProduto As Model.VB.Repository.ICategoriaProdutoRepository
    Public ReadOnly Property CategoriaProduto As Model.VB.Repository.ICategoriaProdutoRepository Implements Model.VB.Repository.IUnitOfWork.CategoriaProduto
        Get
            Return Me._categoriaProduto
        End Get
    End Property

    Private _produto As Model.VB.Repository.IProdutoRepository

    Public ReadOnly Property Produto As Model.VB.Repository.IProdutoRepository Implements Model.VB.Repository.IUnitOfWork.Produto
        Get
            Return Me._produto
        End Get
    End Property

    Public Sub New()
        MyBase.New
        Me.Contexto = New Contexto
        Me._categoriaProduto = New CategoriaProdutoRepository(Me.Contexto.CategoriaProduto)
        Me._produto = New ProdutoRepository(Me.Contexto.Produto)
    End Sub

    Public Sub Save() Implements Model.VB.Repository.IUnitOfWork.Save
        Me.Contexto.SaveChanges()
    End Sub
End Class

Note que a implementação não é muito complicada. Nós só temos as variáveis e propriedades para cada um dos repositórios, além de uma variável para o contexto do Entity Framework. Os repositórios são instanciados no construtor passando os respectivos DbSets do contexto. Além disso, o método “Save” simplesmente chama um “SaveChanges” no contexto do Entity Framework.

E agora chegou a hora da grande “mágica“. Se nós adicionarmos uma referência no nosso projeto Console Application para essa nova “Class Library” onde o repositório do Entity Framework foi implementado, nós conseguimos trocar completamente a estratégia de armazenamento de dados para a nossa aplicação. Ao invés de armazenarmos em memória, nós armazenaremos no banco de dados utilizando o Entity Framework:

            // C#
            contexto = new EF.UnitOfWork();
' VB.NET
Contexto = New EF.VB.UnitOfWork()

Execute a aplicação e veja o resultado:

E o banco de dados foi criado automaticamente por trás dos panos no LocalDb:

Dificuldades do Repository com ADO.NET puro ou Dapper

Será que é possível utilizarmos o padrão Repository e Unit of Work com ADO.NET puro ou Dapper? Apesar de ser possível, nós teremos algumas dificuldades se quisermos seguir esse padrão em que o repositório não é responsável pela persistência dos dados, mas sim o Unit of Work. Nesse caso teríamos que controlar o estado das entidades para vermos se elas foram alteradas, e aí sim fazermos um INSERT, UPDATE ou DELETE no banco de dados dependendo das alterações que foram feitas.

Uma alternativa seria trazer a responsabilidade da atualização dos dados para o repositório (ou seja, nós teríamos um método “Update” nele também), descartando então o Unit of Work. Porém, isso tornaria a implementação meio que incompatível com ORMs como Entity Framework e NHibernate.

Quem sabe em uma próxima oportunidade eu tente explorar essas duas possibilidades, mas hoje nós vamos ficando por aqui.

Baixe o projeto de exemplo

Para baixar o projeto de exemplo desse artigo, assine a minha newsletter. Ao fazer isso, além de ter acesso ao projeto, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário no final do artigo.

Concluindo

O armazenamento de informações é uma parte crucial de qualquer aplicação de negócios. Muitas vezes, nós acabamos implementando a aplicação de maneira completamente acoplada a uma estratégia de persistência de dados (ADO.NET, Entity Framework, NHibernate, Dapper, etc). Aplicações desenvolvidas dessa maneira funcionam muito bem até o momento em que precisamos trocar a camada de armazenamento de dados.

Para desacoplarmos o acesso a dados da aplicação, nós podemos utilizar o padrão Repository e Unit of Work. Outra vantagem que temos ao utilizarmos esses padrões é a concentração de consultas customizadas em um único lugar, evitando repetição de código.

Você já trabalhou com esses design patterns? Como foram as suas experiências com eles? Eu mencionei no início do artigo que eu nunca trabalhei profissionalmente com eles, mas depois de ter escrito esse artigo eu consegui entender a importância deles e muito provavelmente vou acabar utilizando esses padrões em novas aplicações no futuro. Fico aguardando as suas observações nos comentários logo abaixo.

Até a próxima!

André Lima

Newsletter do André Lima

* indicates required



Powered by MailChimp

8 thoughts on “Desacoplando o acesso a dados da aplicação com o padrão Repository e Unit of Work

  • Claudio disse:

    Parabéns pelo conteúdo….seria bom se tivesse prosseguimento no mesmo tema, antes de ir para um próximo….para o aluno não perder o foco!!

    • andrealveslima disse:

      Olá Claudio, muito obrigado pelo comentário!

      Infelizmente não vou conseguir dar continuidade com esse tema nas próximas semanas, uma vez que eu já tinha outros conteúdos prontos e agendados.. Mas, pode ficar tranquilo que já está nos meus planos a segunda parte desse artigo..

      Abraço!
      André Lima

  • Fausto Luís disse:

    Muito Bom. Iniciantes devem ter ficado com “água na boca”…

    Em relação a “… teríamos que controlar o estado das entidades para vermos se elas foram alteradas, e aí sim fazermos um INSERT, UPDATE ou DELETE no banco de dados dependendo das alterações que foram feitas.”, penso que o artigo + comentários em “https://www.codeproject.com/Articles/581487/Unit-of-Work-Design-Pattern” poderá ser esclarecedor.

    Um ou outro pormenor que poderá querer incluir na sua agenda:

    – DI (Direct Injection);
    – UoW genérico () para quando temos muitas tabelas/POCO’s para gerir;
    – Factory para gerir conexões (através da leitura do app.config, p. exº, podermos escolher com facilidade, qual o data provider que iremos utilizar (com recurso ao IDbConnection, p. exº));
    – Implementação no repositório UnitOfWork, da operação Save/Commit/Complete e consequente Rollback.

    Abraço

    • andrealveslima disse:

      Olá Fausto!

      Muito obrigado pelo comentário e pelas sugestões de temas.. Vou colocá-los aqui na minha lista.. :)

      Abraço!
      André Lima

  • Thamires disse:

    O InMemory encontramos em qual lib/namespace?

    • andrealveslima disse:

      Olá Thamires!

      “InMemory” foi só o nome do projeto que eu escolhi para colocar a implementação do repositório que só persiste as informações em memória.. Não é nenhuma referência ou namespace específico do .NET..

      Abraço!
      André Lima

  • Anderson Roberto de Oliveira disse:

    Ótimo artigo André!

    Eu tenho uma dúvida, utilizando Entity Framework seria correto criar um método no repositório que retorne um IQueryable?
    Se não, o que seria recomendado caso eu queira fazer Join entre as entidades onde as mesmas não possuem Navigation Properties?

    Obrigado!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *