Es habitual en desarrollo encontrarnos una y otra vez con los mismos problemas de un proyecto a otro e incluso en el mismo proyecto. Tendemos a solucionarlos siempre de una misma manera, conocida y cuyos resultados y efectos conocemos también. A finales de los años 80, Ward Cunningham y Kent Beck decidieron aplicar la idea de los patrones de diseño que Christopher Alexander aportó al mundo de la arquitectura. Pensaron que podrían abstraerse del problema concreto y expresar la solución proporcionada en términos de patrones genéricos que pudieran ser reusados una y otra vez. Quizás el sector no estaba preparado para aplicar patrones de diseño en su código. Habría que esperar a 1994 para que Gamma, Helm, Johnson y Vlissides, conocidos como The Gans of Four [GoF] escribieran Design Patterns, Elements of reusable Object-Oriented Software. En él describen 23 patrones comunes y su influencia en la programación es equivalente a la del método científico moderno de Galileo para la ciencia actual. Sin embargo, no es hasta año 2003 que no oímos hablar por primera vez del patrón Repositorio de mano de Martin Fowler en su libro Patterns of Enterprise Application Arrchitecture [Fowler, PEAA], y cuya autoría adjudica a Edward Hieatt y Rob Mee.

¿Para qué sirve?

El patrón repositorio (repository pattern en la literatura anglosajona), es un patrón sofisticado que usa otros patrones para poder articularse.

“Un mecanismo para encapsular los comportamientos de almacenamiento, obtención y búsqueda que emulan una colección de objetos.”. Eric Evans en Domain-Driven Design, [Evans, DDD]

O, dicho de otro modo, es un patrón para aislar los tecnicismos de la persistencia de las capas superiores y en especial del modelo de Dominio. Gracias a ello podremos concentrarnos en implementar en el modelo las reglas de negocio. El cliente ahora podrá concentrarse en cómo presentar la información, en cómo aplicar filtros, algoritmos, relaciones y reglas que hagan cumplir las reglas de empresa especificadas. Otra ventaja de usar este patrón es que podemos usar distintos tipos de persistencia, ya sea a la vez o por sustitución. Por ejemplo, podemos usar SQL Server para almacenar los usuarios, Oracle para los productos, y Excel para las órdenes de Compra, o mañana sustituir la base de datos de Oracle por Azure SQL. Derivado de esto podemos implementar un Repositorio en memoria, que nos sirva para hacer pruebas de test unitarias (Test Driven Development, TDD) sobre nuestro modelo de negocio sin que afecte a los datos reales, y así probar una y otra vez las capas superiores sin que por ello afecte a las capas inferiores.

Planificando la implementación del Repositorio

Como decía más arriba este patrón se apoya en otros patrones, entidad, objeto valor, agregado, unidad de trabajo (UoW), objeto consulta, especificación, … Para empezar, debemos haber diseñado nuestro modelo de dominio, con sus entidades, objeto valor, agregados y sus relaciones. Sólo implementaremos el patrón repositorio a los agregados del modelo de dominio.

“Para cada tipo de objeto que necesita un acceso global, crea un objeto que muestre la ilusión de una colección en memoria de objetos de ese tipo. Configura el acceso a través de una interfaz global bien conocida. Proporciona métodos para añadir y borrar objetos, que encapsularán la actual inserción y borrado de datos en el origen de datos. Proporciona métodos que seleccionen objetos o colecciones de objetos cuyos atributos –valor coincidan con el criterio, encapsulando la actual tecnología usada para almacenar y consultar datos. Proporciona REPOSITORIOS solo para AGREGADOS raíz que necesiten acceso directo. Mantén al cliente enfocado en el modelo, delegando todos los accesos y almacenamientos de objetos a los REPOSITORIOS.” [Evans, DDD]

¡Ojo! Aquí al usar un ORM (Object Relational Mapper), sobre nuestra base de datos. Si usamos una técnica Data Base First puede que obtengamos el famoso talón de Aquiles de los ORM, la explosión de tipos. Si podemos elegir, mejor implementar un Code First. Si no, porque por ejemplo estamos usando una base de datos existente, tened en cuenta que el modelo de datos del ORM, no es el modelo de Dominio por lo que es en el Repositorio dónde implementaremos una conversión de un modelo a otro, ya sea con el uso del patrón mapeado, el Adaptador… Lo mismo pasa con la UoW del ORM, que en muchas ocasiones consiste en la base de datos entera, lo que es carísimo en términos de uso de memoria y nos impide actuar en detalle sobre los problemas técnicos de la persistencia de cada agregado. Con Code First nos ahorraremos bastante trabajo. Por lo tanto, Nunca implementaremos el Repositorio sobre el modelo de datos, sino sobre el modelo Dominio.

En .Net es fácil que optemos por Entity Framework (EF) como ORM y DbContext como objeto del que heredar nuestro UoW. Habrá aún que especificar

Por otro lado, tenemos que haber implementado las distintas UoW que necesitaremos para nuestros Repositorios. Los repositorios que trabajen con una persistencia que controle transacciones, concurrencia, bloqueos… Necesitarán una UoW completa, y los que no, como un repositorio en memoria, la UoW consistirá exclusivamente en una colección de objetos.

Ahora toca elegir qué estrategia elegiremos para las consultas. Tenemos por un lado el patrón especificación y por el otro el patrón consulta. En .Net si queremos usar el patrón consulta lo tenemos fácil pues LINQ es en sí mismo un patrón consulta, que ya está implementado y altamente probado, por lo que nuestra tarea se facilita. Además implementa el patrón Fluent Interface (Applying Domain-Driven Design and Patterns with Examples in C# and .Net, Jimmy Nilsson) [Nilsson, ADDDP]. Es el momento de estudiar los distintos patrones de infraestructura, Lazy Loading/Eager Loading [Evans, DDD], Identity Map [Evans, DDD], Concurrency [Nilsson, ADDDP], …

¿Cómo implementarlo?

En la Web de Fowler [Fowler, PEAA], podemos ver explicado este patrón como una colección de interfaces. Lo cierto es que una explicación más concreta la podemos encontrar en [Evans, DDD]. Una búsqueda en Bing nos arroja bastantes resultados y ejemplos. Sin embargo, muchos de ellos ofrecen una implementación, que, aunque económica en cuanto a tiempo invertido por el programador, resultan del todo erróneas, declarando una interfaz con todos los métodos para todos los repositorios de nuestra aplicación y luego usan genéricos para especificar la implementación concreta del repositorio. Al hacer esto no tenemos en cuenta las necesidades específicas de nuestro agregado. Así, por ejemplo, podemos tener un agregado ‘Clientes’, que no deseemos que exponga al consumidor de nuestro repositorio el método ‘Borrar()’, o la ‘Orden de Compra’, que luego circulará por el módulo Almacén y el módulo transportista, y que no querremos permitir los métodos de ‘Borrar()’ o ‘Actualizar()’. Así mismo no desearemos, por regla general, extraer todos los objetos de una colección de un agregado, para prevenir cosas como ‘GetAllProductos()’. En general las extracciones deberán ser limitadas, ya sea por paginación ya sea por filtro o por consulta, y ordenadas.

Podemos si deseamos usar una interfaz por cada tipo de acción a implementar para que sean heredadas por nuestra interfaz del repositorio específico. Así nuestro ‘IClientesRepositorio’ puede heredar de ‘ICanUpdateRepository()’. Aun así, creo que es mejor, siempre que se pueda, implementar la interfaz entera sin herencias, para obligarnos a pensar con cuidado los métodos que necesitamos con cada Repositorio.

Pues bien, una vez que definimos la interfaz del repositorio asociado a nuestro agregado, que establecerá el contrato con nuestra capa de dominio toca implementarlo. La implementación la hacemos en una clase que hereda de la citada interfaz, y en su interior implementamos por dependencia la UoW que será nuestro almacén de objetos y quien controle transacciones, bloqueos, carga de datos… No olvidar que el repositorio habla hacia afuera en términos que entiende el modelo de Dominio, como entidades, agregados, especificaciones, listas, y hacia dentro en términos que entiende la implementación concreta de la persistencia, como SQL, LINQ, IQueryables, Entidades de EF, UoW, transacciones, bloqueos…

En todo momento deberemos tener presente el uso que estamos haciendo de la memoria, y la CPU, ya que el Repositorio puede ser la diferencia entre una aplicación altamente eficiente y un proyecto fracasado.

Una Prueba de Concepto (POC)

Vamos a ver un ejemplo concreto, con el fin de asentar los conceptos anteriores. Pongamos que tengo que hacer un repositorio para Productos. Empezaremos por implementar la interfaz de IProductosRepositorio. Tras evaluar concienzudamente los requisitos de nuestra aplicación, hemos determinado exactamente los tipos de métodos que necesitamos implementar.


public
interface
IProductoRepository

{

IProductoUnitOfWork UnitOfWork { get; }

Producto GetByID(object id);

void Insert(Producto entity);

void Update(Producto entityToUpdate);

IEnumerable<Producto> GetPagedElements<TKey>(int pageIndex, int pageCount, Expression<Func<Producto, TKey>> orderByExpression, bool ascending = true);

}

Tras ello implementamos la clase

public
class
ProductoRepository : IProductoRepository

{

IProductoUnitOfWork unitofwork;


public
IProductoUnitOfWork UnitOfWork

{

get { return
this.unitofwork; }

}


public ProductoRepository(IProductoUnitOfWork unitofwork)

{

this.unitofwork = unitofwork;

}


public
virtual
Producto GetByID(object id) =>

return unitofwork.Set<Producto>().Find(id);


public
virtual
void Insert(Producto entity) =>

unitofwork.Set<Producto>().Add(entity);


public
virtual
void Update(Producto entity) =>

unitofwork.SetModified<Producto>(entity);


public
IEnumerable<Producto> GetPagedElements<TKey>(int Index, int Count,


Expression<Func<Producto, TKey>> orderBy, bool ascending = true)

{

if (Index < 1)

Index = 1;

if (orderBy == (Expression<Func<Producto, TKey>>) null)

throw
new
ArgumentNullException();

return (ascending)

? unitofwork.Set<Producto>().OrderBy(orderBy)

.Skip((Index – 1) * Count)

.Take(Count)

.ToList()

: unitofwork.Set<Producto>().OrderByDescending(orderBy)

.Skip((Index – 1) * Count)

.Take(Count)

.ToList();

}

}

Hay que tener mucho cuidado de no convertir en Anti patrón el repositorio, usándolo de manera correcta y sólo cuando sea necesario, evaluando seriamente si nuestra aplicación puede ser mejor sin él.

En próximas entradas del blog exploraré más detalladamente otros patrones.

David Lozano Rivada