Repository e Unit Of Work Pattern
Um dos padrões mais utilizados quando usamos DDD, mesmo que seu projeto use um DDD meio falsificado, é o Repository Pattern e em alguns casos em conjunto com Unit Of Work. Existem varias implementações desses padrões, no qual eu concordo com algumas e descordo de outras.
Lembre – se, padrões foram feitos para resolver problemas existentes, se você tem um problema novo, faça algo novo!
Repository Pattern
Segundo Eric Evans:
“Um repositório representa todos os objetos de um determinado tipo como sendo um conjunto conceitual (normalmente imitado). Ele age como uma coleção. Objetos do tipo adequado são acrescentados ou removidos e o maquinário existente por trás do repositório os insere ou os exclui do banco de dados. Essa definição une um conjunto coeso de responsabilidades para fornecer acesso as raizes de agregação desde o inicio do ciclo de vida ate o seu final.” [DDD, Evans, p: 143]
Segundo Vernon:
“Um repositório comumente se refere a um local de armazenamento, geralmente considerado um lugar de segurança ou preservação dos itens armazenados nele. Ao armazenar algo em um repositório e depois retornar para recupera – lo, espera – se que ele estará no mesmo estado em que se encontrava quando você o colocou la.” [DDD, Vernon, p: 401]
Segundo Dumbá:
“Um repositório é simplesmente o lugar onde você vai guardar e recuperar as informações do seu objeto. Sendo ele sua raiz de agregação. ” 🙂
Quem pensou que seria onde você faz o CRUD, os puritanos vão me matar, mas é exatamente isso! Mas, todavia, entretanto, com algumas particularidades. O repositório deve representar em memória a persistência do o seu objeto, seja ele em banco de dados, TXT, papel de pão ou embaixo da cadeira.
Lembrando que se você enche seu repositório de consultas para filtros ou relatórios, possivelmente terá um outro padrão chamado DAO, “Data Access Object”. Isso pode ser representado em outro lugar. Veja o Estilo CQRS por exemplo (Esse video é perfeito sobre o assunto).
Mas Dumbá, eu coloco todas as minhas Queries no repositório e retorno meus DTOs, estou errado??
Errado você não está, mas também não esta certo. Como diz o outro: “Se funciona e porque esta certo” 🙂
Mas porque eu não gosto disso?
Por que geralmente o repositório ira representar sua Raiz de Agregação (AggragateRoot) e ele, na teoria, teria somente os métodos Create, Update, GetByID e Delete, alguns ainda colocam o Count.
Então o repositório representa o famoso CRUD!!!?? Calma!
Na verdade, você pode sim colocar um retorno de uma coleção do seu objeto, lembrando que o objeto deve ser de sua Raiz de Agregação. Se eu entupo ele de selects ate mesmo enxugar um objeto, possivelmente isso seria o caso de retornar um DTO e não o meu objeto em especifico. Te aconselho a fazer de uma outra forma. Lembre – se do CQRS.
Mas posso ter DTO com Repository?
Sim, mas não no seu repositório, de preferencia. Crie um “QuerieRepository” para você nao ficar triste.
Exemplos:
Interface de um repository:
public abstract class Repository<T, TId> where T : AggregateRoot<TId> { protected DbSet<T> _dbSet; protected Repository(DbContext context) { _dbSet = context.Set<T>(); } public virtual void Create(T entity) { _dbSet.Add(entity); } public virtual void Update(T entity) { _dbSet.Update(entity); } public virtual T GetByID(TId id) { return _dbSet.FirstOrDefault(x => x.Id.Equals(id)); } public virtual void Remove(T entity) { _dbSet.Remove(entity); } }
Interface de um “QueryRepository”
public abstract class QueryRepository { readonly DbContext _context; protected QueryRepository(DbContext context) { _context = context; } /// <summary> /// Faz o select em seu banco de dados usando o LINQ do Entity Framework (EF) /// </summary> /// <typeparam name="TEntity">Sua classe de entidade</typeparam> protected IEnumerable<TEntity> SelectLinq<TEntity>(Func<TEntity, bool> predicate) where TEntity: class { return _context.Set<TEntity>().Where(predicate); } /// <summary> /// Faz o select em seu banco de dados /// </summary> /// <typeparam name="Tdto"></typeparam> protected IList<Tdto> SelectSql<Tdto>(string sql, params object[] parameters) where Tdto: class { using var command = _context.Database.GetDbConnection().CreateCommand(); command.CommandText = sql; foreach (var parameter in parameters) { command.Parameters.Add(parameter); } _context.Database.OpenConnection(); using var result = command.ExecuteReader(); return DataReaderMapToList<Tdto>(result); } private static List<Tdto> DataReaderMapToList<Tdto>(DbDataReader dr) { List<Tdto> list = new List<Tdto>(); if (dr.HasRows) { while (dr.Read()) { var dto = Activator.CreateInstance<Tdto>(); foreach (PropertyInfo prop in dto.GetType().GetProperties()) { if (dr.GetColumnSchema().Any(x => x.ColumnName.ToUpper() == prop.Name.ToUpper())) { if (!Equals(dr[prop.Name], DBNull.Value)) { prop.SetValue(dto, dr[prop.Name], null); } } } list.Add(dto); } return list; } return new List<Tdto>(); } }
Unit Of Work
A grande resolução de problemas deste padrão se da para a resposta a pergunta:
Muito bom o repositório, mas como eu controlo um contexto transacional em duas raizes de agregação? É ai que o unit of work entra com os quatro pé na porta!
Segundo Fowler em seu site:
“Uma Unit of Work, mantém uma lista de objetos afetados por uma transação comercial e coordena a gravação de alterações e a resolução de problemas de simultaneidade.”
Ele funciona como um controle de transações entre os repositórios, como uma transação de banco de dados, sim puritanos é o melhor exemplo de comparação, mas lembre – se que nada é tão simples quanto parece, pois tudo isso deve servir não somente para banco de dados, mas para qualquer meio que persista seu objeto.
É aqui que vejo enumeras implementações diferentes. Erradas? Não!! Mas tenho menos afinidades com algumas que vou explicar o porque.
Para quem não sabe, o Entity Framework e o NHibernate ja implementam esse padrão de projetos em conjunto com o Repository.
Mas Dumbá, ja utilizo o Entity nos meus projetos, então porque vou reinventar a roda?
Você vai fazer isso porque não quer o seu projeto fortemente acoplado com um framework e é um Dev esperto ja que está lendo esse artigo.
Uma dica é que todos esses padrões e estilos arquiteturais escritos no mundo tem somente um objetivo em comum, o Desacoplamento. Assunto para um próximo artigo.
Usando o exemplo do Entity, o seu DBContext seria sua UnitOfWork e seus DBSets seriam os seus repositórios.
Esse é o modelo mais divulgado, mas no meu ponto de vista o mais mal implementado. Ele funciona muito bem quando temos mapas de contextos muito bem definidos nos projetos e não um sistema com um único Unit Of Work e 3856 repositórios.
public class UnitOfWork { IRepository1 Repository1 { get { return new Repository1(); } } IRepository2 Repository2 { get { return new Repository2(); } } IRepositoryN RepositoryN { get { return new RepositoryN(); } } void Commit() {} void Rollback() {} }
Nessa implementação, temos uma classe de UnitOfWork que compõem todas as classes do seu contexto de repositório. Assim qualquer chamada de repositório para seu objeto passa necessariamente pela sua UnitOfWork.
Pontos que não gosto.
- Na maioria das vezes temos um contexto com 3978 repositórios mapeados na Unit Of Work, pois na grande maioria das empresas somente trabalha com um contexto. “DDD meio doente”
- Forte acoplamento, pois para cada chamada de Get teríamos uma construção da classe de repositório.
- Testabilidade. Acho que nem sei como testar um negocio desse. 🙂
- E pra mim, muito pra mim, ele fere o principio da responsabilidade única, pois na minha interpretação a UnitOfWork deveria somente controlar a persistência e não os repositórios, mas isso é a minha opinião.
Uma outra implementação seria a mesma citada acima, porem com a injeção de dependência dos repositórios, melhorando a Testabilidade e o forte Acoplamento. Mas acrescento o Overhead de memória. Imagine ter instanciando 3000 repositórios ao usar a Unit Of Work?
public class UnitOfWork { public readonly IRepository1 _repository1; public readonly IRepository2 _repository2; public readonly IRepositoryN _repository3; public UnitOfWork(IRepository1 repository1, IRepository2 repository2, IRepositoryN repository3) { _repository1 = repository1; _repository2 = repository2; _repository3 = repository3; } void Commit() {} void Rollback() {} }
Uma outra seria também muito parecida, porém recuperada por Service Locator. Nesse ponto ate gosto mais, pois ai fica somente a minha particularidade da Responsabilidade Única do SOLID, mas ai entra o Service Locator que não me simpatizo muito com ele, outra opinião minha e somente minha. Mas cabe aqui um outro artigo.
public class UnitOfWork { IRepository GetRepository<TRepository>() where TRepository: IRepository { //Implemente seu ServiceLocator. } void Commit() {} void Rollback() {} }
Lembrando que tudo isso pode ser minimizado utilizando os mapas de contextos de forma correta, pois assim o projeto não teria uma única UnitOfWork somente, mas uma por contexto.
Eu ja prefiro fazer de forma separada. Cada um no seu quadrado.
Dessa forma, tenho um “DbConnection” que pode ser sua implementação para banco de dados (dbContext por exemplo), TXT, Papel de Pão, etc. Na implementação da UnitOfWork e do Repository, temos uma composição da classe DbConnection. Como nas duas classes temos a mesma instancia de DbConnection, temos o controle transacional. Assim passo a UnitOfWork e somente os repositórios que desejo trabalhar evitando todos os problemas relatados anteriormente.
public class CreateLogCommandHandler : CommandHandler<CreateLogCommand, Guid> { readonly ILogRepository _logRepository; readonly IProjectRepository _projectRepository; public CreateLogCommandHandler( IValidationService validationService, IUnitOfWork uow, ILogRepository logRepository, IProjectRepository projectRepository) : base(validationService, uow) { _logRepository = logRepository; _projectRepository = projectRepository; } public override async Task<Guid> Handle(CreateLogCommand request, CancellationToken cancellationToken) { if (!this.IsValidCommand(request)) { return await Task.FromResult(Guid.Empty); } try { var project = _projectRepository.GetByApiKey(request.ApiKey); if (project == null) { _validationService.AddErrors("01", "Projeto nao encontrado para a api-key informada."); return await Task.FromResult(Guid.Empty); } var log = new Log(request.Description, request.Source, request.LogType, project); await _logRepository.CreateAsync(log); await _uow.CommitAsync(); return log.Id; } catch (DomainException ex) { _validationService.AddErrors("02", ex.Message); return await Task.FromResult(Guid.Empty); } } }
O que gosto é o certo??
Pra alguns sim, para outros não, mas o importante e não se prender.
Dumbá, não preciso controlar transações de repositórios diferentes.
Então coloque um método de Commit no seu repositório de não utilize o Unit Of Work.
No meu GitHub vai encontrar como eu utilizo esse padrões nesse projeto. Duvidas e sugestões é só mandar!!
Lembre – se que nem eu, nem ninguém além de você entende o seu contexto de desenvolvimento. Bom código não significa que você colocou todo os padrões do mundo nele. Bom código, no meu ponto de vista, é o que resolve o problema do seu cliente, que seja capaz de evoluir e que qualquer um consiga manter. Pense nisso.
Bibliografia
https://martinfowler.com/eaaCatalog/unitOfWork.html
http://www.macoratti.net/16/01/net_uow1.htm
https://www.devmedia.com.br/unit-of-work-o-padrao-de-unidade-de-trabalho-net/25811
https://www.youtube.com/watch?v=DZZSmllmydc
https://www.youtube.com/watch?v=FeqpZ9w-CRA
https://docs.microsoft.com/pt-br/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
https://balta.io/blog/dapper-unit-of-work-repository-pattern
Deixe uma resposta
Want to join the discussion?Feel free to contribute!