Blog >

Specification-pattern


Actualmente estamos realizando un proyecto de migración de gran envergadura en el que es necesario «tirar» de un buen número de patrones de diseño y buenas prácticas.

Uno de los patrones que nos ha servido de base en toda la migración es el patrón Specification.

El patrón specification nos permite definir el criterio que queremos que cumpla un objeto, o un conjunto de objetos, independientemente de cómo estén éstos construidos y del mecanismo que se utilice para obtenerlos.

Una especificación puede modelarse como un objeto, el cual contiene un método (IsSatisfiedBy()), que devuelve verdadero o falso, en función de la lógica que se desea evaluar en su interior (comparaciones de fechas, condiciones impuestas por el negocio, etc.)

Existen tres tipologías fundamentales:

  • Hard-Coded: Codificamos los criterios de selección en el método IsSatisfiedBy() como un bloque de código.
  • Parametrizada: Creamos atributos en la especificación para valores que normalmente varien. Codificamos el método IsSatisfiedBy() para combinar.
  • Compuestas: Especificación que se compone de otras especificaciones anidadas mediante operadores lógicos And, Or, y Not.

Este patrón está explicado, a nivel lógico y en detalle, en un artículo conjunto de Martin Fowler y Eric Evans: PDF

El patrón original sólo define el uso de las especificaciones para objetos en memoria; sería deseable por tanto aplicar este patrón a las búsquedas en BBDD.
Existe una complejidad: Traducir una especificación a SQL para construir una cláusula WHERE.
Gracias a la flexibilidad que ofrece LINQ, podemos construir especificaciones que nos permitirán realizar 2 tipos de operaciones:

  • Utilizar objetos en memoria
  • Utilizar el DbContext de Entity Framework, el cual soporta búsquedas de objetos basadas en especificaciones.

De esta forma, nos abstraemos del mecanismo de recuperación de datos (memoria o BBDD), y nos concentramos en definir de manera clara un concepto o regla de negocio, que reside en un único lugar. Se facilita el mantenimiento.

[sourcecode language=»csharp»]
public abstract class Specification<TEntity> : ISpecification<TEntity>
where TEntity : class
{
public abstract Expression<Func<TEntity, bool>> SatisfiedBy();

public static Specification<TEntity> operator &(Specification<TEntity> leftSideSpecification, Specification<TEntity> rightSideSpecification)
{
return new AndSpecification<TEntity>(leftSideSpecification, rightSideSpecification);
}

public static Specification<TEntity> operator |(Specification<TEntity> leftSideSpecification, Specification<TEntity> rightSideSpecification)
{
return new OrSpecification<TEntity>(leftSideSpecification, rightSideSpecification);
}

public static Specification<TEntity> operator !(Specification<TEntity> specification)
{
return new NotSpecification<TEntity>(specification);
}

public static bool operator false(Specification<TEntity> specification)
{
return false;
}

public static bool operator true(Specification<TEntity> specification)
{
return true;
}
}
[/sourcecode]

[sourcecode language=»csharp»]
public class DirectSpecification<TEntity> : Specification<TEntity>
where TEntity : class
{
private Expression<Func<TEntity, bool>> _matchingCriteria;

public DirectSpecification(Expression<Func<TEntity, bool>> matchingCriteria)
{
_matchingCriteria = matchingCriteria;
}

public DirectSpecification(string expression, params object[] values)
{
_matchingCriteria = System.Linq.Dynamic.DynamicExpression.ParseLambda<TEntity, bool>(expression, values);

}

public DirectSpecification()
{
}

public override System.Linq.Expressions.Expression<Func<TEntity, bool>> SatisfiedBy()
{
return _matchingCriteria;
}

}
[/sourcecode]

[sourcecode language=»csharp»]
public class AndSpecification<TEntity> : CompositeSpecification<TEntity>
where TEntity : class
{
ISpecification<TEntity> _leftSideSpecification = null;
ISpecification<TEntity> _rightSideSpecification = null;

public override ISpecification<TEntity> LeftSideSpecification
{
get { return _leftSideSpecification; }
}

public override ISpecification<TEntity> RightSideSpecification
{
get { return _rightSideSpecification; }
}

public AndSpecification(ISpecification<TEntity> leftSide, ISpecification<TEntity> rightSide)
{
_leftSideSpecification = leftSide;
_rightSideSpecification = rightSide;
}

public override Expression<Func<TEntity, bool>> SatisfiedBy()
{
Expression<Func<TEntity, bool>> left = _leftSideSpecification.SatisfiedBy();
Expression<Func<TEntity, bool>> right = _rightSideSpecification.SatisfiedBy();

return left.And(right);
}
}
[/sourcecode]

Especificación compuesta por varias especificaciones:

[sourcecode language=»csharp»]
private static Specification<ExpedienteFacturacion> PlusUltraBancoValencia()
{
Specification<ExpedienteFacturacion> spec = ExpedientePlusUltra();
spec &= SiniestroHogar();
Specification<ExpedienteFacturacion> specAgente = new DirectSpecification<ExpedienteFacturacion>(e => e.Poliza.AgeCodigo == "P28099");
Specification<ExpedienteFacturacion> specProducto = new DirectSpecification<ExpedienteFacturacion>(e => e.Poliza.Producto.Descripcion.ToUpper().Contains("BANCAJA"));
specProducto |= new DirectSpecification<ExpedienteFacturacion>(e => e.Poliza.Producto.Descripcion.ToUpper().Contains("BANCO VALENCIA"));
specProducto |= specAgente;

return spec && specProducto;
}
[/sourcecode]

Especificación sobre una colección de objetos en memoria:
Para poder filtrar por la expresión de LINQ, debemos compilar primero la expresión devuelta por la especificación.

Especificación sobre una colección de objetos que residen en BBDD:
Pasamos la especificación a la unidad de trabajo, la cual se encarga de compilar la expresión y traducirla a T-SQL.

[sourcecode language=»csharp»]
var repository = new Repository<ExpedienteFacturacion>(_unitOfWork);
var specification = FacturaSpecifications.PlusUltraBancoValencia();
var list = repository.AllMatching(specification);
[/sourcecode]

  1. Las especificaciones nos permiten «encapsular» la lógica de negocio para establecer criterios, que nos permitirán seleccionar objetos, validarlos, etc.
  2. Criterios/especificaciones simples: pueden ser utilizadas para componer otras especificaciones mas complejas.
  3. Gracias a LINQ, podemos usar los árboles de expresiones para componer especificaciones, y usar éstas indistintamente para trabajar con objetos en memoria, o para acceder a objetos en BBDD.
  4. Las especificaciones aumentan el grado de mantenimiento y reutilización del código.

 

Si queréis comentarnos lo que sea, podéis hacerlo en info@kabel.es.

También podéis seguirnos en Twitter y LinkedIn.

 

Kabel Geek


 

Suscríbete a nuestra newsletter para enterarte de las novedades más Geek

Newsletter Banner
RGPD

Contenido Relacionado