En diseño de software, el patrón de especificación (y en su caso particular, criteria o filter) es un patrón de diseño, mediante el cual, se permite filtrar una colección de objetos bajo diversos criterios, encadenándolos de una manera desacoplada por medio de operaciones lógicas.
Este patrón se utiliza en escenarios específicos, donde la obtención de uno o más entidades depende de reglas de negocio. Fue creado por Eric Evans y Martin Fowler[1]
Propósito
Filtrar colecciones de objetos según diversos criterios de forma desacoplada.
Permitir la reutilización y anidación de criterios por medio de operaciones lógicas (and, or, not). Generar una manera legible y extensible de agregar o quitar lógica para filtrar colecciones de objetos.
Motivación
Frecuentemente, se necesita filtrar colecciones de objetos de la misma familia (clase base) utilizando criterios similares, pero en distinto orden y/o condición.
También a menudo, esta lógica para filtrar colecciones es un proceso clave o complejo como pueden ser los criterios de planificación en un Sistema Operativo o el manejo de prioridades para una cola de peticiones.
Otra caso sería el tener altas posibilidades de cambio en el futuro sobre la lógica que realiza el filtrado en esta colección de objetos. Sea quitando/modificando criterios existentes, como así también agregando nuevos.
Este patrón pone gran énfasis en la extensibilidad y reutilización de criterios, como así también en la legibilidad del código.
Ventajas
- Extensibilidad
- Legibilidad
- Reutilización de criterios
- Facilidad para hacer test unitarios (cada criterio se puede probar independiente, el patrón asegura el uso colectivo)
- Activación de criterios en tiempo de ejecución (dependiendo del lenguaje de programación)
Estructura
Estructura básica del patrón independiente del lenguaje de programación.
Estructura .NET
Aquí se presenta para .NET, utilizando métodos de extensión
Implementación (C#)
public interface ICriteria<E>
{
List<E> MeetCriteria(List<E> entities);
}
internal class AndCriteria<E> : ICriteria<E>
{
private ICriteria<E> _criteria;
private ICriteria<E> _otherCriteria;
internal AndCriteria(ICriteria<E> criteria, ICriteria<E> otherCriteria)
{
_criteria = criteria;
_otherCriteria = otherCriteria;
}
public List<E> MeetCriteria(List<E> entities)
{
var result = _criteria.MeetCriteria(entities);
// Si ya devuelve 1, es que uno solo cumplio el criterio y no
// se ejecutan los ands anidados
// Si ya devuelve 0, es que ninguno cumplio con el criterio y
// no se ejecutan los ands anidados
if (result.Count == 0 || result.Count == 1)
return result;
return _otherCriteria.MeetCriteria(result);
}
}
internal class OrCriteria<E> : ICriteria<E>
{
private ICriteria<E> _criteria;
private ICriteria<E> _otherCriteria;
internal OrCriteria(ICriteria<E> criteria, ICriteria<E> otherCriteria)
{
_criteria = criteria;
_otherCriteria = otherCriteria;
}
public List<E> MeetCriteria(List<E> entities)
{
List<E> firstCriteriaItems = _criteria.MeetCriteria(entities);
List<E> otherCriteriaItems = _otherCriteria.MeetCriteria(entities);
foreach (E otherCriteriaItem in otherCriteriaItems)
{
if(!firstCriteriaItems.Contains(otherCriteriaItem))
firstCriteriaItems.Add(otherCriteriaItem);
}
return firstCriteriaItems;
}
}
internal class NotCriteria<E> : ICriteria<E>
{
private ICriteria<E> _criteria;
internal NotCriteria(ICriteria<E> x)
{
_criteria = x;
}
public List<E> MeetCriteria(List<E> entities)
{
List<E> notCriteriaItems = _criteria.MeetCriteria(entities);
foreach (E notCriteriaItem in notCriteriaItems)
entities.Remove(notCriteriaItem);
return entities;
}
}
public static class CriteriaExtensionMethods
{
public static ICriteria<E> And<E>(this ICriteria<E> criteria, ICriteria<E> otherCriteria)
{
return new AndCriteria<E>(criteria, otherCriteria);
}
public static ICriteria<E> Or<E>(this ICriteria<E> criteria, ICriteria<E> otherCriteria)
{
return new OrCriteria<E>(criteria, otherCriteria);
}
public static ICriteria<E> Not<E>(this ICriteria<E> criteria)
{
return new NotCriteria<E>(criteria);
}
}
Ejemplo de uso
Suponiendo la existencia de la clase Persona
public class Persona
{
public int Edad { get; set; }
public string Nombre { get; set; }
public string Descripcion { get; set; }
public string Apellido { get; set; }
public Sexo Sexo { get; set; }
public Origen Origen { get; set; }
public EstadoMarital EstadoMarital { get; set; }
public Profesion Profesion { get; set; }
}
Y creando estos criterios:
public class CriterioMasculinos : ICriteria<Persona>
{
public List<Persona> MeetCriteria(List<Persona> entities)
{
var hombres = from h in entities
where h.Sexo == Sexo.Masculino
select h;
return hombres.ToList();
}
}
public class CriterioFemeninos : ICriteria<Persona>
{
public List<Persona> MeetCriteria(List<Persona> entities)
{
var mujeres = from m in entities
where m.Sexo == Sexo.Femenino
select m;
return mujeres.ToList();
}
}
public class CriterioExtranjeros : ICriteria<Persona>
{
public List<Persona> MeetCriteria(List<Persona> entities)
{
var personas = from h in entities
where h.Origen == Origen.Extranjero
select h;
return personas.ToList();
}
}
public class CriterioSolteros : ICriteria<Persona>
{
public List<Persona> MeetCriteria(List<Persona> entities)
{
var personas = from h in entities
where h.EstadoMarital == EstadoMarital.Soltero
select h;
return personas.ToList();
}
}
Podríamos filtrar a los hombres extranjeros y solteros
ICriteria<Persona> masculino = new CriterioMasculinos();
ICriteria<Persona> femenino = new CriterioFemeninos();
ICriteria<Persona> soltero = new CriterioSolteros();
ICriteria<Persona> extranjero = new CriterioExtranjeros();
/* ---------- HOMBRES EXTRANJEROS Y SOLTEROS ---------- */
criterios = masculino.And(extranjero).And(soltero);
foreach (var persona in criterios.MeetCriteria(personas))
Console.WriteLine(persona.Descripcion);
Console.ReadLine();
O a las mujeres extranjeras
ICriteria<Persona> masculino = new CriterioMasculinos();
ICriteria<Persona> femenino = new CriterioFemeninos();
ICriteria<Persona> soltero = new CriterioSolteros();
ICriteria<Persona> extranjero = new CriterioExtranjeros();
/* ---------- MUJERES EXTRANJERAS ---------- */
criterios = femenino.And(extranjero); // esto seria lo mismo que masculino.Not().And(extranjero)
foreach (var persona in criterios.MeetCriteria(personas))
Console.WriteLine(persona.Descripcion);
Console.ReadLine();
O también podríamos filtrar a los hombres o mujeres (a todos)
ICriteria<Persona> masculino = new CriterioMasculinos();
ICriteria<Persona> femenino = new CriterioFemeninos();
ICriteria<Persona> soltero = new CriterioSolteros();
ICriteria<Persona> extranjero = new CriterioExtranjeros();
/* ---------- HOMBRES O MUJERES ---------- */
criterios = masculino.Or(femenino);
foreach (var persona in criterios.MeetCriteria(personas))
Console.WriteLine(persona.Descripcion);
Console.ReadLine();
Referencias
- ↑ Specifications by Eric Evans and Martin Fowler
Enlaces externos