"Subindo" um pouco o nível da série de posts sobre design patterns, falarei hoje sobre o pattern "Repository".
Este pattern não é tão amplamente difundido, apesar de sua grande flexibilidade e poder de resolver nosso velho dilema de como isolar a persistência das demais camadas permitindo uma arquitetura plugável onde podemos utilizar múltiplos bancos de dados (na verdade, uma implementação concreta de repositório pode persistir dados em qualquer lugar, não necessariamente direto em um banco de dados.
Um repositório media o relacionamento entre o dominio e os dados. Imagine um repositório literalmente: é um lugar onde são guardadas coisas e coleções de coisas e onde efetuamos pesquisas dessas coisas.
Fonte: http://www.martinfowler.com/eaaCatalog/repository.html
Como podemos observar, nosso domínio (ou camada de negócios) irá sempre acessar o repositório para efetuar qualquer operação sobre os dados (inclusão, alteração, consulta e exclusão). O repositório então se encarregará da comunicação da implementação concreta da persistência.
O termo “concreta” no parágrafo anterior já nos dá a dica de que teremos abstração. Para implementação de repositórios sempre usaremos Interfaces, o que garantirá a isolação total das camadas. Essas interfaces deverão ter métodos para adição, exclusão, alteração e pesquisa. Normalmente teremos vários métodos para pesquisa, apropriados para cada situação.
Uma das primeiras perguntas que podem aparecer é como iremos trafegar os dados entre as camadas. Neste pattern não é necessária a criação de DTOs, VOs ou qualquer outra implementação anêmica semelhante: iremos enviar os próprios objetos de negócio como parâmetro para os métodos do repositório.
Outra dúvida, de resposta não muito fácil, é a dificuldade de determinar quais repositórios deverão ser criados. Academicamente falando, levando ao pé da letra, deveríamos criar um repositório para cada classe de negócio. Se tivéssemos as classes Cliente, Endereco, Estado, deveríamos ter as interfaces IClienteRepository, IEnderecoRepository e IEstadoRepository. Com certeza essa forma é muito mais organizada e clara, mas com o tempo se percebe que ela exige muito mais codificação, principalmente na hora de usarmos efetivamente o repositório. Tenho utilizado como regra agrupar classes relacionadas em um único repositório, por exemplo: as classes Logradouro, Bairro, Cidade, Estado, Pais, fariam parte de um repositório chamado “ILocalizacaoRepository”.
Por último, surge a dúvida de como instanciar as classes concretas. Para resolver isso temos que usar outros design patterns como por exemplo, o Factory ou o Provider Model (ou ainda os 2 em conjunto). O Provider Model é mais indicado por ser mais flexível: você não precisa referenciar as dlls das classes concretas em tempo de projeto, você pode plugá-las em tempo de execução.
Exemplo de implementação do Pattern Repository em C#
Como o Provider Model é um tanto complexo para implementar, vou usar apenas o Factory para criar um exemplo de código usando Repository, dessa forma mantemos o foco no assunto. Futuramente pretendo escrever um post sobre o Provider Model.
Para implementarmos o Repository precisamos ter no mínimo o Modelo do Domínio (nossas regras de negócio), a layer de abstração do Repositório e uma ou mais layers concretas de implementação do repositório. O correto é que cada layer concreta esteja em uma class library distinta, exemplo: uma para Sql Server, outra pra Oracle, etc. No nosso exemplo,para facilitar o entendimento, iremos colocar tudo em um único projeto.
Iremos implantar os seguintes ítens:
a) Classes do modelo do dominio (duas classes simples);
b) Uma Interface de repositório para cada classe do modelo;
c) Um Factory de tipos de repositórios, que retornará um conjunto de repositórios (conjunto “mysql”, conjunto “sql server”,etc)
d) Um Factory de repositório, que retornará a classe concreta que implementa o repositório dentro do tipo retornado no item anterior.
e) Um tipo de repositório MemoryRepository que irá persistir os dados em memória RAM. Obviamente, em um sistema real a persistência será feita no banco de dados.
a) Classes do modelo do dominio
Para identificar as classes “cobertas” pelo nosso repositório irei criar uma Interface “IClasseNegocio”, mas ela não é fundamental para o pattern Repository.
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
namespace Exemplo
{
public interface IClasseNegocio
{
int Id { get; set; }
}
}
Agora, as duas classes do Modelo:
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using Exemplo;
namespace Entidades
{
public class Cliente:IClasseNegocio
{
public int Id { get; set; }
public string Nome { get; set; }
public override string ToString()
{
return string.Format("{0} - {1}", this.Id, this.Nome);
}
}
}
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using System;
using Exemplo;
using Exemplo.Repository;
using Repositorios;
namespace Entidades
{
public class Pedido : IClasseNegocio
{
public int Id { get; set; }
public DateTime Data { get; set; }
public int? CodigoCliente
{
get
{
return _codigoCliente;
}
set
{
_codigoCliente = value;
}
}
public Cliente _cliente;
public int? _codigoCliente = null;
public Cliente Cliente
{
get
{
if (_cliente == null && CodigoCliente.HasValue)
{
_cliente = Repository<IClienteRepository>.GetRepository().Carregar(CodigoCliente.Value);
return null;
}
return _cliente;
}
set
{
this._cliente = value;
this._codigoCliente = value.Id;
}
}
public decimal ValorPedido { get; set; }
public override string ToString()
{
return String.Format("Pedido {0} para {1} no valor de {2}", this.Id,
this.Cliente.Nome, this.ValorPedido.ToString());
}
}
}
No caso da classe de pedidos criei uma forma de “lazy loading” um tanto tabajara (mas funciona!). Perceba que já faço uma chamada ao repositório, mas isso veremos com mais detalhes para frente.
b) Uma Interface de repositório para cada classe do modelo
Todas os repositórios precisarão descender de uma mesma interface IRepository<T>. Essa interface descenderá de outra não genérica para permitir determinadas operações (bem como a utilização com WCF e Remoting, que não permitem algumas operações com generics).
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using System.Collections.Generic;
namespace Exemplo.Repository
{
public interface IRepositoryBase
{
}
public interface IRepository<T> : IRepositoryBase where T : IClasseNegocio
{
T Carregar(int id);
List<T> Lista();
void Adicionar(T objeto);
void Atualizar(T objeto);
void Excluir(T objeto);
}
}
Percebam que limitei a utilização de repositórios às classes de negócio.
Deixei definidas alguns métodos que pretendo ter em todos os repositórios.
c) Um Factory de tipos de repositórios
O factory de tipos de repositório irá retornar uma conjunto de repositórios apropriados. Por exemplo, podemos, através de configuração, que iremos utilizar um repositório que implementa persistência no Sql Server, ou outro que persiste no Oracle ou ainda outro que acessa via remoting algum server.
Recomendo que essa configuração seja feita através do Pattern Provider, dessa forma a configuração pode ser feita “por fora” do código, sem necessidade de referenciar previamente as DLLs dos repositórios.
Em nosso exemplo criarei apenas um repositório simples que persiste os dados em memória, por isso retornarei sempre o mesmo repositório, sem configuração alguma:
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
namespace Exemplo.Repository
{
public static class Repository<T>
{
static RepositoryFactoryProvider providerPadrao;
public static RepositoryFactoryProvider ProviderPadrao
{
get
{
if (providerPadrao == null)
{
providerPadrao = new MemoryRepository.MemoryRepository();
}
return providerPadrao;
}
set
{
providerPadrao = value;
}
}
public static T GetRepository()
{
return ProviderPadrao.GetRepository<T>();
}
}
}
d) Um Factory de repositório
O factory do item c acima retorna qual tipo de repositório iremos usar. Uma vez determinado qual tipo, precisamos determinar qual implementação concreta de repositório iremos utilizar. Isso será feito pelo código mais abaixo. Perceba que esse código é a implementação da chamada new MemoryRepository.MemoryRepository().
Precisaremos também de uma interface comum para as fábricas de repositórios.
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
namespace Exemplo.Repository
{
public abstract class RepositoryFactoryProvider
{
public abstract T GetRepository<T>();
}
}
E abaixo, a implementação concreta do factory para nosso MemoryProvider. Perceba que teremos um desses para cada tipo de repositório:
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using System;
using System.Linq;
using System.Reflection;
using Exemplo.Repository;
namespace Exemplo.MemoryRepository
{
public class MemoryRepository :RepositoryFactoryProvider
{
public override T GetRepository<T>()
{
var tipo = Types.Where(t => t.GetInterface(typeof(T).Name) != null).FirstOrDefault();
return (T)Activator.CreateInstance(tipo);
}
public static Type[] _types = null;
/// <summary>
/// Retorna uma lista de repositórios suportado no Assembly corrente
/// </summary>
private Type[] Types
{
get
{
if (_types == null)
{
_types = Assembly.GetExecutingAssembly().GetTypes().
Where(t => t.GetInterface(typeof(IRepositoryBase).Name) != null
&& t.Namespace == this.GetType().Namespace
).ToArray<Type>();
}
return _types;
}
}
}
}
Para não ter que implementar um mega case para avaliar o tipo T passado, através desse código crio uma lista de tipos do assembly corrente que implementam IRepositoryBase. Depois, a cada necessidade, procuro nessa lista qua repositório preciso retornar para o chamador e crio uma instância dele.
e) Os repositórios concretos
Por último precisamos implementar os repositórios definidos no item b, ou seja, implementar efetivamente a persistência dos dados:
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Entidades;
using Repositorios;
namespace Exemplo.MemoryRepository
{
public class ClienteMemoryRepository: IClienteRepository
{
private static Dictionary<int, Cliente> clientes = new Dictionary<int, Cliente>();
#region IRepository<Cliente> Members
public Entidades.Cliente Carregar(int id)
{
if (clientes.ContainsKey(id))
{
return clientes[id];
}
else
{
throw new System.Exception("Cliente não encontrado");
}
}
public List<Entidades.Cliente> Lista()
{
return clientes.Values.ToList();
}
public void Adicionar(Entidades.Cliente objeto)
{
if (objeto.Id > 0)
{
throw new Exception("Cliente já cadastrado");
}
else
{
if (clientes.Count > 0)
{
objeto.Id = clientes.Values.Max(c => c.Id) +1;
}
else
{
objeto.Id = 1;
}
clientes.Add(objeto.Id, objeto);
}
}
public void Atualizar(Entidades.Cliente objeto)
{
clientes[objeto.Id] = objeto;
}
public void Excluir(Entidades.Cliente objeto)
{
clientes.Remove(objeto.Id);
}
#endregion
}
}
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Entidades;
using Repositorios;
namespace Exemplo.MemoryRepository
{
public class PedidoMemoryRepository :IPedidoRepository
{
private static Dictionary<int, Pedido> Pedidos = new Dictionary<int, Pedido>();
#region IRepository<Pedido> Members
public Entidades.Pedido Carregar(int id)
{
if (Pedidos.ContainsKey(id))
{
return Pedidos[id];
}
else
{
throw new System.Exception("Pedido não encontrado");
}
}
public List<Entidades.Pedido> Lista()
{
return Pedidos.Values.ToList();
}
public void Adicionar(Entidades.Pedido objeto)
{
if (objeto.Id > 0)
{
throw new Exception("Pedido já cadastrado");
}
else
{
if (Pedidos.Count > 0)
{
objeto.Id = Pedidos.Values.Max(c => c.Id);
}
else
{
objeto.Id = 1;
}
Pedidos.Add(objeto.Id, objeto);
}
}
public void Atualizar(Entidades.Pedido objeto)
{
Pedidos[objeto.Id] = objeto;
}
public void Excluir(Entidades.Pedido objeto)
{
Pedidos.Remove(objeto.Id);
}
#endregion
}
}
Perceba que não é nada muito complexo. Apenas estou manipulando os dados na memória do computador.
Caso você venha a ter outros tipos de repositório (como um que persista em Sql Server), você precisará implementar essas classes para cada tipo de repositório.
Como usar o repositório
Um pequeno detalhe na utilização do Pattern Repository é a necessidade de uma pequena via-crucis para chamar cada método. Veja abaixo uma chamada completa:
Cliente cli = Repository<IClienteRepository>.GetRepository().Carregar(1);
Vamos “desconstruir” a chamada:
| Repository |
<IClienteRepository> |
GetRepository() |
Carregar |
(1) |
|
1
|
2
|
3
|
4
|
5
|
1 – Chamada ao factory de tipos de repositórios criado no item “C” mais acima. Perceba que é transparente qual tipo de repositório estou usando, afinal, não interessa para a camada de negócios como os dados são persistidos.
2 – Repositório desejado, no caso, repositório de clientes.
3 – Ao chegarmos neste ponto, já estamos no factory de repositório definido no item “d” acima, pois a chamada a Repository determinou o tipo de repositório utilizado e passou a bola para o método GetRepository<T> do mesmo.
4 – Método desejado. Como já passamos pelos dois factories, aqui já estaremos na implementação concreta do método Carregar.
5 – Parâmetro do método, ou seja, estou solicitando o cliente de Id “1”
Quando preciso fazer uma série de chamadas em sequência a um repositório, costumo declarar uma variável do tipo IRepository<T> e usar esta variável. Veja exemplo:
IClienteRepository repCliente = Repository<IClienteRepository>.GetRepository();
repCliente.Adicionar(new Cliente() { Nome = "xpto" });
repCliente.Adicionar(new Cliente() { Nome = "xpto1" });
Veja abaixo um exemplo de utilização do repositório com chamadas a vários métodos:
/* Exemplo de uso de Pattern Repository
* Autor: Cássio Rogério Eskelsen
* http://www.bizness.com.br
*/
using System;
using Entidades;
using Exemplo.Repository;
using Repositorios;
namespace Exemplo
{
class Program
{
static void Main(string[] args)
{
IPedidoRepository repPedido = Repository<IPedidoRepository>.GetRepository();
IClienteRepository repCliente = Repository<IClienteRepository>.GetRepository();
repCliente.Adicionar(new Cliente() { Nome = "xpto" });
repCliente.Adicionar(new Cliente() { Nome = "xpto1" });
repCliente.Adicionar(new Cliente() { Nome = "xpto2" });
repCliente.Adicionar(new Cliente() { Nome = "xpto3" });
foreach (Cliente cli in repCliente.Lista())
{
Console.WriteLine(cli);
}
Pedido ped = new Pedido();
ped.Cliente = repCliente.Carregar(1);
ped.Data = DateTime.Now;
ped.ValorPedido = 100;
repPedido.Adicionar(ped);
foreach (Pedido p in repPedido.Lista())
{
Console.WriteLine(p);
}
Console.Read();
}
}
}
O código acima irá produzir a seguinte saída:

Finalizando
O pattern Repository permite algumas variações e devido a isso será difícil você encontrar duas implementações iguais. Uma das variações possíveis é não termos vários repositórios agrupados em um mesmo tipo (por exemplo, um repositório para SQL Server, outro para MySql, etc), e sim tratar cada um individualmente. Isso é muito útil para quando nosso sistema necessitar conversar com vários bancos ao mesmo tempo (exemplo, cadastro de clientes em uma base, dados de vendas em outra,etc).
Alguns fatores não considerei aqui, como por exemplo, controle de transações. Esses detalhes são cobertos por outros design patterns sobre os quais provavelmente falarei no futuro. Dificilmente você irá utilizar o pattern Repository sozinho, sem a companhia de outro design pattern.
Use o espaço de comentários abaixo para dúvidas, sugestões e considerações sobre minha implementação.