Blog dedicado a la programación .NET y la informática en general

Entradas etiquetadas como ‘Patrones’

El Patrón Especificación en .NET bajo proyectos DDD

Llevo tiempo detrás de este patrón, ya que como definición es bastante simple, pero a la hora de llevarla al código ya no lo es tanto si lo que pretendemos es que nos funcione de forma correcta en la capa de datos y trabajando con Entity Framework.

No me voy a centrar mucho en su definición, ya que hay bastante documentación al respecto en internet tanto en inglés como en español. Me voy a centrar más en la necesidad de su codificación y cómo poder usarlo de forma correcta. Este patrón lo he conocido cuando comencé con el libro de Cesar de la Torre sobre la arquitectura DDD. Así que recomiendo que entendáis este concepto antes.

Las clases aquí expuestas las podéis encontrar al completo en mi repositorio de GitHub. El proyecto viene preparado para ser ejecutado bajo SqlServer Express y genera la base de datos de forma automática. Además la solución tiene una estructura aproximada a DDD.

Comencemos por el principio ¿qué es el patrón especificación?

Martin Fowler y Eric Evans lo describen en el siguiente enlace. Yo lo voy a simplificar insultantemente desde el concepto de orientación al dominio:

Recordemos que DDD es un conglomerado de buenas prácticas en las que se encuentra SOLID, y precisamente una de ellas es la «separación de responsabilidades» o «Principio de Responsabilidad Única», y separar las operaciones de negocio (la funcionalidad que realiza una empresa con esa aplicación) de las operaciones de base de datos (la ejecución de scripts para recuperar información o manipularla) es una de ellas. Bien, pues bajo esta premisa se quiere facilitar en la capa de negocio una serie de operaciones sobre sus entidades que ignoren por completo cómo se va a resolver en las llamadas a base de datos. Es decir, y metiéndonos en la definición del patrón especificación, separa las responsabilidades de «qué se puede hacer con una entidad» de «cómo lo va a hacer». Por tanto hay que definir un mecanismo que nos permita traspasar esas expresiones de consulta hasta la capa de base de datos, para que allí las pueda interpretar y adaptar.

Tenéis más detalles del patrón en el libro «Arquitectura N-Capas DDD .NET 4.0» en las secciones 2.2.6 y 3.6.

Implementación básica

La principal premisa que debe realizar una especificación es indicar si satisface una condición. Por lo que empezaremos por una implementación sencilla, aunque incompleta, que aproveche las bondades de los árboles de expresión, ya que nos ofrece una potencia de codificación importante. Comenzamos con la interfaz:

using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas.PatronBasico
{
    public interface ISpecification<TEntity>
    {
        Expression<Func<TEntity, bool>> IsSatisfiedBy();
    }
}

Ahora la clase que la va a implementar:

using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas.PatronBasico
{
    public abstract class Specification<TEntity> : ISpecification<TEntity>
    {
        public abstract Expression<Func<TEntity, bool>> IsSatisfiedBy();
    }
}

Bien ya tenemos los cimientos, ahora vamos a crear una especificación de consulta bajo su estructura:

using PatronEspecificacion.Dominio.Consultas.PatronBasico;
using PatronEspecificacion.Dominio.Entidades;
using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas
{
    public class DireccionesPorProvinciaSpecificationBasico : Specification<DireccionEspanolaEntity>
    {
        private readonly string provincia;

        public DireccionesPorProvinciaSpecificationBasico(string provincia) : base()
        {
            this.provincia = provincia;
        }

        public override Expression<Func<DireccionEspanolaEntity, bool>> IsSatisfiedBy()
        {
            return (x) => x.Provincia == this.provincia;
        }
    }
}

En este punto tenemos los cimientos y una consulta, vamos a aplicarlo en la capa de datos para que Entity Framework lo utilice:

using PatronEspecificacion.InfraestructuraDatos.Persistencia;
using System;
using System.Collections.Generic;
using System.Linq;
using PatronEspecificacion.Dominio.Contratos;
using PatronEspecificacion.Dominio.Entidades;
using System.Linq.Expressions;
using PatronEspecificacion.InfraestructuraDatos.Base;

namespace PatronEspecificacion.InfraestructuraDatos.Repositorios
{
    public class DireccionesRepository : IDireccionesRepository
    {
        public ICollection<DireccionEspanolaEntity> GetDireccionesBasico(Dominio.Consultas.PatronBasico.ISpecification<DireccionEspanolaEntity> especificacion)
        {
            ICollection<DireccionEspanolaEntity> salida;

            using (PoCEspecificacionContext ctx = new PoCEspecificacionContext())
            {
                IQueryable<DireccionEspanolaEntity> query = ctx.Direcciones.Select(d => new DireccionEspanolaEntity
                {
                    Id = d.DireccionId,
                    Provincia = d.Provincia,
                    Municipio = d.Municipio,
                    Calle = d.Calle
                })
                .Where(especificacion.IsSatisfiedBy());
                string parada = query.ToSql();
                salida = query.ToList();
            }

            return salida;
        }
    }
}

Quiero ver qué tal se acoplaba dicha consulta en las consultas que terminan generando «Entity Framework». Para ello hay dos formas de comprobarlo, ejecutando «SQLProfiler» y depurando las llamadas que recibe el servidor de base de datos, o implementando una extensión a «IQueryableExtensions» que he encontrado aquí y que extrae el script que finalmente se lanza sobre la BBDD. Por tanto extraigo la consulta SQL en la variable «parada» y pararé la depuración en ese punto para comprobarlo.

Ahora consumimos los datos desde el proyecto de negocio:

using PatronEspecificacion.Dominio.Entidades;
using PatronEspecificacion.Dominio.Servicios.Interfaces;
using System.Collections.Generic;
using PatronEspecificacion.Dominio.Contratos;
using PatronEspecificacion.Dominio.Consultas;

namespace PatronEspecificacion.Dominio.Servicios
{
    public class GestorDirecciones : IGestorDirecciones
    {
        private IDireccionesRepository direccionesRepository;

        public GestorDirecciones(IDireccionesRepository repo)
        {
            direccionesRepository = repo;
        }

        public string MyProperty { get; set; } = "Un valor";

        public ICollection<DireccionEspanolaEntity> ObtenerDirecciones()
        {
            // Consulta por la implementación básica, no admite combinaciones
            var espBas = new DireccionesPorProvinciaSpecificationBasico("Madrid");
            var dirBas = direccionesRepository.GetDireccionesBasico(espBas);

            return dirBas ;
        }
    }
}

Cuando esto se ejecuta se puede comprobar que funciona correctamente. Si nos detenemos incluso en la variable «parada» (antes comentada) se puede ver que efectivamente la consulta SQL integra el filtro.

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE [d].[Provincia] = N'Madrid'

Implementación de Wikipedia

El problema viene cuando queremos realizar varias consultas y que interactúen entre ellas, aquí entra la implementación más compleja para incluir operaciones AND, OR y NOT, y se necesita una especificación compuesta de otras especificaciones. Por lo que el código se enfoca en desarrollar la siguente estructura obtenida de la WikiPedia:

El código lo podéis consultar en su web. El ejemplo que he tomado utiliza las expresiones en arbol y los tipos genéricos. Si se implementa en el proyecto, el resultado es que se pueden realizar consultas complejas de otras más simples.

// Combinación de consultas por la implementación según Wikipedia
var espWiki1 = new DireccionesPorProvinciaSpecificationWiki("Madrid");
var espWiki2 = new DireccionesPorMunicipioSpecificationWiki("Madrid");
var dirWiki = direccionesRepository.GetDireccionesWiki(espWiki1.And(espWiki2));

Pero ¿qué tal se integra en las consultas de «Entity Framework»? Veamos la variable «parada«.

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]

¡Vaya se trae toda la tabla sin filtrar! El ejemplo de la WikiPedia está orientado a objetos en memoria, no en base de datos, por lo que no está optimizado para usarlo con «Entity Framework» y esto con una tabla de millones de registros puede ser un problema.

Implementación del proyecto ejemplo de DDD

Demos una vuelta de tuerca más, tomemos la implementación del patrón según el proyecto de ejemplo DDD que hace unos años estaba publicado por Cesar de la Torre. El código fuente lo dejo subido en GitHub, aquí tan sólo comento las diferencias.

A primera vista vemos que implementa los comando AND, OR y NOT mediante los operadores ‘&’, ‘|’ y ‘!’ con lo cual la codificación queda de la siguiente manera:

// Combinación de consultas por la implementación del proyecto DDD
var espDdd1 = new DireccionesPorProvinciaSpecificationDdd("Madrid");
var espddd2 = new DireccionesPorMunicipioSpecificationDdd("Madrid");
var dirDdd = direccionesRepository.GetDireccionesDdd(espDdd1&espddd2);

Pero ¿solo es esto?… pues NO. Esta implementación también tiene en cuenta que va a ejecutarse sobre «Entity Framework» por LinQ y por tanto están integradas las composiciones de especificaciónes en el código SQL resultante. Para ello usa las clases «ExpressionBuilder» y «ParameterRebinder «.

Volvamos a parar la depuración en la variable «parada» y comprobémoslo:

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE (CASE
    WHEN [d].[Provincia] = N'Madrid'
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END & CASE
    WHEN [d].[Municipio] = N'Madrid'
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) = 1

Efectivamente, los datos ya vienen filtrados desde SqlServer.

Pero aquí no queda la cosa, también disponemos de otra clase llamada «DirectSpecification» que nos permite potenciar las especificaciones de consulta. Por tanto en el código donde disponía de dos especificaciones, «DireccionesPorProvinciaSpecificationDdd» y «DireccionesPorMunicipioSpecificationDdd» las termino uniendo en una sola de la siguiente forma:

using PatronEspecificacion.Dominio.Consultas.PatronDdd;
using PatronEspecificacion.Dominio.Entidades;
using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas
{
    public class DireccionesFiltradasSpecificationDdd : Specification<DireccionEspanolaEntity>
    {
        private readonly DireccionEspanolaFiltro filtro;

        DireccionesFiltradasSpecificationDdd(DireccionEspanolaFiltro filtro)
        {
            this.filtro = filtro;
        }

        public override Expression<Func<DireccionEspanolaEntity, bool>> SatisfiedBy()
        {
            Specification<DireccionEspanolaEntity> spec = new TrueSpecification<DireccionEspanolaEntity>();

            if (!string.IsNullOrWhiteSpace(filtro.Provincia))
            {
                spec &= new DirectSpecification<DireccionEspanolaEntity>(d => d.Provincia == (filtro.Provincia));
            }
            if (!string.IsNullOrWhiteSpace(filtro.Municipio))
            {
                spec &= new DirectSpecification<DireccionEspanolaEntity>(d => d.Municipio == (filtro.Municipio));
            }
            if (filtro.Exclusion != null)
            {
                spec &= new NotSpecification<DireccionEspanolaEntity>(new DireccionesFiltradasSpecificationDdd(filtro.Exclusion));
            }

            return spec.SatisfiedBy();
        }
    }
}

En la misma se puede ver como utilizar las clases «DirectSpecification» y «NotSpecification«, esto da muchas posibilidades de codificación.

Ahora codifico una prueba donde obtengo las direcciones de la provincia de Madrid excluyendo al municipio de Madrid:

// Ahora excluyamos a Madrid capital
filtro = new DireccionEspanolaFiltro
{
    Provincia = "Madrid",
    Exclusion = new DireccionEspanolaFiltro() { Municipio = "Madrid"}
};
var espddd4 = new DireccionesFiltradasSpecificationDdd(filtro);
var dirExclus = direccionesRepository.GetDireccionesDdd(espddd4);

La consulta SQL que termina generando es la siguiente:

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE ((1 & CASE
    WHEN [d].[Provincia] = N'Madrid'
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) & CASE
    WHEN NOT ((1 & CASE
        WHEN [d].[Municipio] = N'Madrid'
        THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
    END) = 1)
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) = 1

Por tanto se integra perfectamente y nos permite consultas con exclusiones en los resultados.

¿Pero esto no se puede hacer pasando los árboles de expresión como parámetro?

Pues SI, al menos en la prueba que yo he realizado he conseguido que la expresión se integre correctamente en la consulta de «Entity Framework».

public ICollection<DireccionEspanolaEntity> GetDirecciones(Expression<Func<DireccionEspanolaEntity, bool>> exp)
{
	ICollection<DireccionEspanolaEntity> salida;

	using (PoCEspecificacionContext ctx = new PoCEspecificacionContext())
	{
		IQueryable<DireccionEspanolaEntity> query = ctx.Direcciones
			.Select(d => new DireccionEspanolaEntity
			{
				Id = d.DireccionId,
				Provincia = d.Provincia,
				Municipio = d.Municipio,
				Calle = d.Calle
			})
			.Where(exp);
		string parada = query.ToSql();
		salida = query.ToList();
	}

	return salida;
}

El resultado del SQL es el siguiente:

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE ([d].[Provincia] = N'Madrid') AND ([d].[Municipio] = N'Madrid')

Pero esto no quita interés al patrón, creo que la experiencia conociéndolo ha sido interesante.

Conclusión

Este artículo se ha escrito con un fin didáctico personal y si lo publico es porque creo que puede resultar interesante a otras personas, no soy un experto del patrón, por lo que cualquier crítica constructiva será bienvenida. Por tanto queda abierta a posibles ediciones a posteriori.

El patrón me ha permitido averiguar cómo poder separar las responsabilidades de consultas de negocio respecto a su implementación en base de datos y adentrarme más profundamente en los árboles de expresiones y su uso.

Saludos.

Anuncio publicitario
A %d blogueros les gusta esto: