ASP.NET - Usando o padrão Specification - I
Hoje vou apresentar o padrão Specification e mostrar uma implementação básica em uma aplicação ASP .NET Core usando a Clean Architecture e o EF Core. |
O padrão Specificaton é um padrão de design de software, onde as regras de negócio podem ser recombinadas através de lógica booleana.
Este padrão nos
permite encapsular algum conhecimento de domínio em uma única unidade -
especificação - e reutilizá-lo em diferentes partes da base de código.
Este padrão foi descrito por Eric Evans e mesmo não
sendo uma novidade ele é mais utilizado na abordagem do
Domain Driven Design.
Segundo Martin Fowler e Eric Evans, os principais problemas que este padrão resolve estão relacionados com os seguintes recursos :
Se você quer mais
detalhes veja o paper escrito neste link :
https://martinfowler.com/apsupp/spec.pdf
A seguir vou
apresentar um exemplo de atuação do padrão mostrando o problema que ele resolve
que foi retirado, traduzido e adaptado deste artigo :
https://aspnetboilerplate.com/Pages/Documents/Specifications
Suponha que você tenha um método de serviço que calcula a contagem total de seus clientes conforme mostrado abaixo:
public
class
CustomerManager
//TODO... |
Você provavelmente desejará obter uma contagem de clientes com um filtro. Por exemplo, você pode ter clientes premium (que têm um saldo de mais de R$ 100.000) ou pode querer filtrar os clientes apenas por ano de registro.
Em seguida, você pode criar outros métodos como GetPremiumCustomerCount(), GetCustomerCountRegisteredInYear(int year), GetPremiumCustomerCountRegisteredInYear(int year) e muito mais. Como você tem mais critérios, não é possível criar uma combinação para todas as possibilidades.
Quando temos um
cenário como este onde precisamos geralmente definir um repositório de consultas
que retornam dados onde temos que definir regras com muita variação ou que
podem sofrer constantes alterações em suas regras. Geralmente tendemos a
poluir repositório com dezenas de consultas.
Como consequência acabamos duplicando código no controlador e no repositório que
acabam realizando a mesma tarefa, e assim estamos ferindo os principios SOLID
OCP e SRP e esta violação deixa o código difíciil de manter e testar.
Na programação orientada a objeto, o princípio do
aberto/fechado estabelece que “entidades de software (classes,
módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para
modificação“; isto é, a entidade pode permitir que o seu comportamento seja
estendido sem modificar seu código-fonte. (Wikipedia)
Aqui é que entra o padrão Specification que pode ser usado para recombinar regras de negócio usando logica booleana. Usando este padrão podemos definir regras de negócios em um cenário onde elas podem ser alteradas com frequencia como aplicações tratam com leis e regulações.
Uma solução para o problema apresentando no exemplo seria usar o padrão de especificação onde poderíamos criar um único método que recebe um parâmetro como filtro:
public
class
CustomerManager |
Desta forma, podemos obter qualquer objeto como parâmetro que implemente a interface ISpecification<Customer> que é definida conforme mostrado abaixo:
public
interface ISpecification<T> |
Podemos chamar IsSatisfiedBy com um cliente para testar se esse cliente é pretendido. Dessa forma, podemos usar o mesmo GetCustomerCount com diferentes filtros sem alterar o método em si.
Desta forma Sem usar o padrão Specification acabamos tendo um repositorio generico com diversos métodos para retornar informações usando regras de negócio nas mais diferentes condições.
Seguindo esta
abordagem acabamos temos os seguintes problemas :
1- Código duplicado ferindo o princípio DRY
2- Lógica duplicada no Repositório e no Controller
3- Uso de blocos if/else ferindo o princípio OCP e SRP
Assim fica a pergunta :
Como implementar regras de negócio que podem mudar com o
passar do tempo sem violar o princípio SOLID Aberto/Fechado ou “Open/Closed
Principle” ?
Aqui entra o padrão Specification.
Neste padrão
destacamos os seguines prós e contras:
1- Vantagens
- Código mais facil de manter
- Reaproveitamento do codigo
- Permite definir regras mais claras e expressivas
- Mais facil de testar
2- Desvantagens
- Muito codigo - uma classe para cada tipo de regra
- Especificações compostas são dificeis de explicitar as mensagens
Agora cabe destacar que devemos usar o padrão de forma correta.
Assim, o padrão Specification é indicado quando a
entidade não tem a responsabilidade de validar
uma regra especifica.
Não é objetivo da especificação absorver regras que pertencem à entidade, e sim
absorver regras que NÃO pertencem à entidade.
Desta forma as regras da entidade ficam na entidade.
A seguir vou mostrar uma implementação básica do padrão em uma aplicação ASP .NET Core com EFCore usando a Clean Architecture no ambiente do .NET 7.0.
recursos usados:
Criando a solução e os projetos
Vamos criar uma solução chamada SpecificationPattern contendo 3 projetos :
Vamos usar a abordagem da Clean Architecture simplificada onde o projeto Core representa o Domain e não possui nenhuma dependência.
O projeto ApiDev tem uma dependência e uma referência com o projeto Data e o projeto Data tem uma referência ao projeto Core.
Criando as entidades e interfaces do Domain
No projeto Core crie a pasta Entities e nesta pasta crie as classes Desenvolvedor e Endereco:
1- Desenvolvedor
public class Desenvolvedor
{
public int Id { get; set; }
public string Nome { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int AnosExperiencia { get; set; }
public decimal RendaEstimada { get; set; }
public Endereco? Endereco { get; set; }
}
|
2- Endereco
public class Endereco
{
public int Id { get; set; }
public string Cidade { get; set; } = string.Empty;
public string Localidade { get; set; }= string.Empty;
}
|
Implementando o padrão Speficication
A minha implementação do padrão Specification vai focar no básico e vai seguir o seguinte roteiro:
Assim vamos iniciar criando neste projeto a pasta Specifications onde vamos implementar o padrão Specification usando uma abordagem simplificada. Nesta pasta crie os seguintes artefatos :
1- Interface ISpecification<T>
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>> OrderBy { get; }
Expression<Func<T, object>> OrderByDescending { get; }
}
|
Nesta interface
vamos definir propriedades para tratar com as seguintes especificações :
a- Especificações do filtro (predicate)
b- Especificações de classificação (OrderBy ou OrderByDescending)
c- Cláusula Agrupar por (Group by)
d- Especificações de dados relacionados (Include)
Temos aqui um definição de um contrato com algumas especificações que pode ser
estendido e onde
eu estou focando apenas no principal.
2- Classe BaseSpecification
public class BaseSpecification<T> : ISpecification<T>
{
public BaseSpecification(){}
public BaseSpecification(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
public Expression<Func<T, bool>> Criteria { get; }
public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
public Expression<Func<T, object>> OrderBy { get; private set; }
public Expression<Func<T, object>> OrderByDescending { get; private set; }
protected void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
protected void AddOrderBy(Expression<Func<T, object>> orderByExpression)
{
OrderBy = orderByExpression;
}
protected void AddOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
{
OrderByDescending = orderByDescExpression;
}
}
|
Na classe concreta
BaseSpecification<T> implementamos interface
ISpecification<T>. Nesta classe destacamos que
estamos definindo métodos onde estamos:
- Adicionando as expressões à propriedade Include
- Adicionando as expressões à propriedade OrderBy
- Adicionando as expressões à propriedade OrderByDescending
Note que também temos um construtor que aceita critérios que estamos usando
Expressions
que são uma combinação de operandos (variáveis, literais, chamadas de método)
e operadores que podem ser avaliados em um único valor. As
Expressions representam uma expressão lambda
fortemente tipada como uma estrutura de dados na forma de uma árvore de
expressão.
A seguir vamos definir duas especificações usando o padrão Specification criando as classes
1- DesenvolvedorExperienciaEnderecoSpec
using Core.Entities;
namespace Core.Specifications;
public class DesenvolvedorExperienciaEnderecoSpec : BaseSpecification<Desenvolvedor>
{
public DesenvolvedorExperienciaEnderecoSpec(int anos) : base(x => x.AnosExperiencia > anos)
{
AddInclude(x => x.Endereco);
}
}
|
Esta especificação retorna uma lista de desenvolvedores com experiência de N anos de experiencia ou superior junto com seus endereços.
Então, aqui estamos passando a query expression para Specification que é o construtor de BaseSpecification, que por sua vez irá adicioná-la à propriedade Criteria que havíamos criado anteriormente.
2- DesenvolvedorRendaEstimadaSpec
public class DesenvolvedorRendaEstimadaSpec : BaseSpecification<Desenvolvedor>
{
public DesenvolvedorRendaEstimadaSpec(decimal renda) : base(x => x.RendaEstimada > renda)
{
AddOrderByDescending(x => x.RendaEstimada);
}
}
|
Esta especificação retorna a lista de desenvolvedores na ordem decrescente de salário.
Percebeu que podemos criar quantas especificações precisarmos conforme os critérios definidos nas regras de negócio.
Agora, com nossas classes de especificação prontas, vamos continuar.
A seguir no projeto Core cria a pasta Interfaces e defina a interface IGenericRepository :
public interface IGenericRepository<T> where T : class { Task<T> GetByIdAsync(int id); Task<List<T>> GetAllAsync(); IEnumerable<T> FindWithSpecificationPattern(ISpecification<T> specification = null); } |
Estamos definindo um contrato para obter um desenvolvedor pelo seu id e uma lista de todos os desenvolvedores; e também estamos definindo o método FindWithSpecificationPattern() onde vamos aplicar o padrão specification definindo consultas passando como parâmetro os critérios das consultas.
Criando o mapeamento ORM e implementando o repositório
Vamos agora definir o mapeamento ORM e implementar o repositório genérico no projeto Data.
Crie a pasta Context e a seguir nesta pasta crie a classe AppDbContext:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options)
{ }
public DbSet<Desenvolvedor> Desenvolvedores { get; set; }
public DbSet<Endereco> Enderecos { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Desenvolvedor>()
.Property(x => x.Nome).HasMaxLength(80);
modelBuilder.Entity<Desenvolvedor>()
.Property(x => x.Email).HasMaxLength(150);
modelBuilder.Entity<Desenvolvedor>()
.Property(x => x.RendaEstimada).HasPrecision(10, 2);
modelBuilder.Entity<Endereco>()
.Property(x => x.Cidade).HasMaxLength(50);
modelBuilder.Entity<Endereco>()
.Property(x => x.Localidade).HasMaxLength(200);
}
}
|
A seguir crie a pasta Repositories e nesta pasta cria a classe GenericRepository que implementa a interface IGenericRepository:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly AppDbContext _context;
public GenericRepository(AppDbContext context)
{
_context = context;
}
public async Task<List<T>> GetAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
public async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public IEnumerable<T> FindWithSpecificationPattern(ISpecification<T> specification = null)
{
return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), specification);
}
}
|
A ideia por trás da implementação é criar classes de especificação separadas que possam retornar conjuntos de resultados específicos. Cada uma dessas novas classes de especificação herdará da classe BaseSpecification.
Para concluir esta etapa vamos criar a classe SpecificationEvaluator<T>:
public class SpecificationEvaluator<TEntity>
where TEntity : class
{
public static IQueryable<TEntity> GetQuery(
IQueryable<TEntity> inputQuery,
ISpecification<TEntity> spec)
{
var query = inputQuery;
if (spec.Criteria != null)
{
query = query.Where(spec.Criteria);
}
if (spec.OrderBy != null)
{
query = query.OrderBy(spec.OrderBy);
}
if (spec.OrderByDescending != null)
{
query = query.OrderByDescending(spec.OrderByDescending);
}
query = spec.Includes.Aggregate(query, (current, include) => current.Include(include));
return query;
}
}
|
Nesta classe podemos receber os parâmetros definidos como critério e montar a consulta desejada compondo com IQueryable.
Na próxima parte do artigo vamos definir o código do projeto ApiDev.
"Também não oprimirás o estrangeiro; pois vós
conheceis o coração do estrangeiro, pois fostes estrangeiros na terra do Egito."
Êxodo 23:9
Referências:
NET - Unit of Work - Padrão Unidade de ...