Introducción
El patrón Repository es una técnica ampliamente utilizada en el desarrollo de sistemas modernos. Aparecen referencias a este patrón en obras fundmaentales como el libro de Martin Fowler sobre Patrones de Arquitectura Empresarial (PoEAA) y en los escritos de Eric Evans sobre Diseño Dirigido por el Dominio (DDD). El propósito principal del patrón Repository es establecer una separación clara entre la capa de lógica de negocio (BLL) y la capa de acceso a datos (DAL), permitiendo que la capa de negocio no dependa directamente de las implementaciones concretas del acceso a datos. Además, este patrón ofrece la flexibilidad necesaria para cambiar el origen de datos subyacente sin afectar significativamente el resto del sistema.
El estudio del patrón Repository también introduce el concepto de límites arquitectónicos en el diseño de sistemas. Al aplicar este patrón, es posible encapsular los límites de la arquitectura, isolando sistemas externos, módulos y bases de datos fuera del núcleo de la arquitectura objetivo. Esto resulta en un sistema con mayor cohesión interna y menor acoplamiento entre componentes.
A continuación, se presenta una implementación práctica del patrón Repository que define las responsabilidades e interacciones entre objetos para cumplir con las funcionalidades esperadas de este patrón. El objetivo es proporcionar una referencia útil para desarrolladores que necesiten implementar este enfoque arquitectónico.
Estructura del Sistema
Para ilustrar la implementación, utilizaremos un servicio de conteo de personas llamado ContadorPersonas. Este servicio calcula la cantidad total de personas en el sistema, así como el número de personas de género masculino, proporcionando estos datos a sistemas externos. Se implementará el patrón Repository como límite entre la capa de lógica de negocio y la capa de acceso a datos. La estructura propuesta es la siguiente:
Los componentes principales participantes son:
Persona -Objeto de datos utilizado por el sistema para representar personas. -El identificador PersonaID sirve como clave única para cada objeto.
ContadorPersonas -Utiliza RepositorioPersonas para cargar objetos Persona. -Utiliza los objetos Persona cargados para realizar diversos cálculos de conteo.
RepositorioPersonas -Utiliza IProveedorRepositorioPersonas para cargar objetos Persona.
IProveedorRepositorioPersonas -Interfaz que actúa como límite para el flujo de entrada y salida de objetos Persona. -Proporciona únicamente funcionalidades de consulta.
SqlProveedorRepositorio -Implementa la interfaz IProveedorRepositorioPersonas. -Implementa la lógica de consulta a la base de datos para generar objetos Persona.
A través del siguiente diagrama se puede comprender el flujo de interacción entre los objetos involucrados.
Implementación
Descarga del EjemploPara mayor detalle, consulte el código del ejemplo. Descargar ejemplo RepositoryPattern
Desarrollo del Ejemplo
En primer lugar, se crea el proyecto RepositoryPattern.LogicaNegocio y se define la clase Persona que representa el objeto de datos del sistema.
namespace RepositoryPattern.LogicaNegocio
{
public class Persona
{
// Constructor
public Persona(Guid identificadorPersona)
{
#region Requisito
if (identificadorPersona == Guid.Empty) throw new ArgumentNullException();
#endregion
}
// Propiedades
public Guid PersonaID { get; set; }
public string Nombre { get; set; }
public bool EsMasculino { get; set; }
}
}
A continuación, se crea el objeto RepositorioPersonas y la interfaz IProveedorRepositorioPersonas, que actúan como límite para el flujo de datos hacia la capa de lógica de negocio. (Cabe destacar que también es posible utilizar directamente una interfaz IRepositorioPersonas como límite. La decisión de utilizar una combinación de objeto repositorio e interfaz de proveedor es una preferencia arquitectónica personal, ya que el autor prefiere no exponer interfaces directamente dentro de la arquitectura interna.)
namespace RepositoryPattern.LogicaNegocio
{
public class RepositorioPersonas
{
// Campos
private readonly IProveedorRepositorioPersonas _proveedorRepositorio = null;
// Constructor
public RepositorioPersonas(IProveedorRepositorioPersonas proveedorRepositorio)
{
#region Requisito
if (proveedorRepositorio == null) throw new ArgumentNullException();
#endregion
_proveedorRepositorio = proveedorRepositorio;
}
// Métodos
public IEnumerable<Persona> ObtenerTodas()
{
return _proveedorRepositorio.ObtenerTodas();
}
}
}
namespace RepositoryPattern.LogicaNegocio
{
public interface IProveedorRepositorioPersonas
{
// Métodos
IEnumerable<Persona> ObtenerTodas();
}
}
Seguidamente, se crea el último objeto de la capa de lógica de negocio: el servicio ContadorPersonas, que proporciona la funcionalidad de cálculo de cantidades.
namespace RepositoryPattern.LogicaNegocio
{
public class ContadorPersonas
{
// Campos
private readonly RepositorioPersonas _repositorioPersonas = null;
// Constructor
public ContadorPersonas(RepositorioPersonas repositorioPersonas)
{
#region Requisito
if (repositorioPersonas == null) throw new ArgumentNullException();
#endregion
_repositorioPersonas = repositorioPersonas;
}
// Métodos
public int ObtenerCantidadTotal()
{
return _repositorioPersonas.ObtenerTodas().Count();
}
public int ObtenerCantidadMasculina()
{
int cantidadMasculina = 0;
foreach (Persona persona in _repositorioPersonas.ObtenerTodas())
{
if (persona.EsMasculino == true)
{
cantidadMasculina++;
}
}
return cantidadMasculina;
}
}
}
Ahora se crea el proyecto RepositoryPattern.AccesoDatos y el objeto de acceso a datos SqlProveedorRepositorio para interacturar con SQL. (Como se trata de un ejemplo ilustrativo, en lugar de realizar consultas reales a la base de datos, se crean los datos directamente.)
namespace RepositoryPattern.AccesoDatos
{
public class SqlProveedorRepositorio : IProveedorRepositorioPersonas
{
// Métodos
public IEnumerable<Persona> ObtenerTodas()
{
Persona persona = null;
List<Persona> listaPersonas = new List<Persona>();
persona = new Persona(Guid.NewGuid());
persona.Nombre = "Carlos";
persona.EsMasculino = true;
listaPersonas.Add(persona);
persona = new Persona(Guid.NewGuid());
persona.Nombre = "María";
persona.EsMasculino = false;
listaPersonas.Add(persona);
return listaPersonas;
}
}
}
Finalmente, se crea un proyecto de consola para utilizar el servicio ContadorPersonas. Este proyecto genera una instancia del servicio e imprime los resultados del conteo. (Para este ejemplo ilustrativo, la creación del servicio se realiza directamente. En proyectos reales, se podría utilizar un framework de Inyección de Dependencias para evitar el acoplamiento directo entre el proyecto de consola y el proyecto de acceso a datos.)
namespace RepositoryPattern
{
class Programa
{
static void Main(string[] args)
{
// ContadorPersonas
ContadorPersonas contadorPersonas = CrearContadorPersonas();
// Imprimir resultados
Console.WriteLine("Cantidad Total : " + contadorPersonas.ObtenerCantidadTotal());
Console.WriteLine("Cantidad Masculina : " + contadorPersonas.ObtenerCantidadMasculina());
// Finalizar
Console.ReadLine();
}
static ContadorPersonas CrearContadorPersonas()
{
// ProveedorRepositorio
SqlProveedorRepositorio proveedorRepositorio = new SqlProveedorRepositorio();
// RepositorioPersonas
RepositorioPersonas repositorioPersonas = new RepositorioPersonas(proveedorRepositorio);
// ContadorPersonas
ContadorPersonas contadorPersonas = new ContadorPersonas(repositorioPersonas);
// Retornar
return contadorPersonas;
}
}
}
Escenarios de Uso
A continuación, se presentan varios escenarios para demostrar la capacidad de reutilización del sistema y cómo aprovechar la flexibilidad del patrón Repository para satisfacer diferentes requisitos.
Cambio de Origen de Datos
Al vender un sistema a un cliente, puede darse el caso de que el entorno empresarial del cliente no permita la instalación de SQL Server y sea necesario utilizar otro medio de almacenamiento de datos, como archivos CSV.
En este caso, se puede crear un nuevo objeto CsvProveedorRepositorio según los requisitos del cliente, para reemplazar SqlProveedorRepositorio. El sistema utilizará este nuevo proveedor para consultar los datos de Personas almacenados en archivos CSV. De esta manera, el sistema puede cambiar de origen de datos sin afectar otras partes del sistema. El código de ejemplo es el siguiente:
En primer lugar, dentro del proyecto RepositoryPattern.AccesoDatos, se crea el objeto de acceso a datos CsvProveedorRepositorio para leer archivos CSV. (Como es un ejemplo ilustrativo, en lugar de analizar el contenido del archivo, los datos se crean directamente.)
namespace RepositoryPattern.AccesoDatos
{
public class CsvProveedorRepositorio : IProveedorRepositorioPersonas
{
// Métodos
public IEnumerable<Persona> ObtenerTodas()
{
Persona persona = null;
List<Persona> listaPersonas = new List<Persona>();
persona = new Persona(Guid.NewGuid());
persona.Nombre = "Pedro";
persona.EsMasculino = true;
listaPersonas.Add(persona);
return listaPersonas;
}
}
}
A continuación, dado que el ejemplo no utiliza un framework de Inyección de Dependencias, es necesario modificar manualmente la creación de dependencias.
static ContadorPersonas CrearContadorPersonas()
{
// ProveedorRepositorio
CsvProveedorRepositorio proveedorRepositorio = new CsvProveedorRepositorio();
// RepositorioPersonas
RepositorioPersonas repositorioPersonas = new RepositorioPersonas(proveedorRepositorio);
// ContadorPersonas
ContadorPersonas contadorPersonas = new ContadorPersonas(repositorioPersonas);
// Retornar
return contadorPersonas;
}
Al observar los resultados de ejecución, se puede verificar que el conteo de personas se realiza correctamente utilizando los datos proporcionados por CsvProveedorRepositorio.
Adición de Nuevos Orígenes de Datos
Otro escenario común es cuando, después de un tiempo de uso del sistema, el cliente integra una fuente externa de datos de personas. Esta nueva fuente de datos debe poder utilizarse junto con los datos existentes en el sistema.
En este caso, se puede crear un objeto UnionProveedorRepositorio que combine la nueva fuente de datos de personas con la fuente de datos original. El sistema utilizará este proveedor para obtener datos de Personas de ambas fuentes. De esta manera, el sistema puede incorporar orígenes de datos adicionales. El código de ejemplo es el siguiente:
En primer lugar, dentro del proyecto RepositoryPattern.AccesoDatos, se crea el objeto UnionProveedorRepositorio. Este objeto tiene la capacidad de combinar los datos proporcionados por múltiples implementaciones de IProveedorRepositorioPersonas.
namespace RepositoryPattern.AccesoDatos
{
public class UnionProveedorRepositorio : IProveedorRepositorioPersonas
{
// Campos
private readonly List<IProveedorRepositorioPersonas> _listaProveedores = null;
// Constructor
public UnionProveedorRepositorio(List<IProveedorRepositorioPersonas> listaProveedores)
{
#region Requisito
if (listaProveedores == null) throw new ArgumentNullException();
#endregion
_listaProveedores = listaProveedores;
}
// Métodos
public IEnumerable<Persona> ObtenerTodas()
{
List<Persona> listaPersonas = new List<Persona>();
foreach (IProveedorRepositorioPersonas proveedor in _listaProveedores)
{
foreach (Persona persona in proveedor.ObtenerTodas())
{
listaPersonas.Add(persona);
}
}
return listaPersonas;
}
}
}
Similarly, dado que el ejemplo no utiliza un framework de Inyección de Dependencias, es necesario modificar manualmente la creación de dependencias.
static ContadorPersonas CrearContadorPersonas()
{
// ProveedorRepositorio
List<IProveedorRepositorioPersonas> listaProveedores = new List<IProveedorRepositorioPersonas>();
listaProveedores.Add(new CsvProveedorRepositorio());
listaProveedores.Add(new SqlProveedorRepositorio());
UnionProveedorRepositorio proveedorRepositorio = new UnionProveedorRepositorio(listaProveedores);
// RepositorioPersonas
RepositorioPersonas repositorioPersonas = new RepositorioPersonas(proveedorRepositorio);
// ContadorPersonas
ContadorPersonas contadorPersonas = new ContadorPersonas(repositorioPersonas);
// Retornar
return contadorPersonas;
}
Al observar los resultados de ejecución, se puede verificar que el conteo de personas se realiza correctamente utilizando los datos combinados de CsvProveedorRepositorio y SqlProveedorRepositorio.
Incorporación de Funcionalidad de Caché
Otro escenario común es cuando, después de un período de uso, el cliente observa que el sistema se vuelve lento cuando hay muchos datos. Después de analizar con herramientas de rendimiento, se determina que el análisis de archivos CSV es el cuello de botella del sistema.
Para resolver este problema, se puede crear un objeto CacheProveedorRepositorio que almacene en caché los datos proporcionados por CsvProveedorRepositorio. El sistema solo analizará el archivo CSV la primera vez que se soliciten los datos. De esta manera, el sistema puede incorporar funcionalidad de caché y mejorar el rendimiento. El código de ejemplo es el siguiente:
namespace RepositoryPattern.AccesoDatos
{
public class CacheProveedorRepositorio : IProveedorRepositorioPersonas
{
// Campos
private readonly IProveedorRepositorioPersonas _proveedorRepositorio = null;
private IEnumerable<Persona> _cache = null;
// Constructor
public CacheProveedorRepositorio(IProveedorRepositorioPersonas proveedorRepositorio)
{
#region Requisito
if (proveedorRepositorio == null) throw new ArgumentNullException();
#endregion
_proveedorRepositorio = proveedorRepositorio;
}
// Métodos
public IEnumerable<Persona> ObtenerTodas()
{
if (_cache == null)
{
_cache = _proveedorRepositorio.ObtenerTodas();
}
return _cache;
}
}
}
Por supuesto, dado que el ejemplo no utiliza un framework de Inyección de Dependencias, también es necesario modificar manualmente la creación de dependencias en este caso.
static ContadorPersonas CrearContadorPersonas()
{
// ProveedorRepositorio
CsvProveedorRepositorio proveedorCsv = new CsvProveedorRepositorio();
CacheProveedorRepositorio proveedorRepositorio = new CacheProveedorRepositorio(proveedorCsv);
// RepositorioPersonas
RepositorioPersonas repositorioPersonas = new RepositorioPersonas(proveedorRepositorio);
// ContadorPersonas
ContadorPersonas contadorPersonas = new ContadorPersonas(repositorioPersonas);
// Retornar
return contadorPersonas;
}
Reflexiones Finales
Al analizar la implementación completa del patrón Repository, los desarrolladores observadoresNotarán que tiene cierta similitud con el concepto de Inversión de Control (IoC). La diferencia principal radica en el nivel de granularidad al diseñar la separación de dependencias. IoC se enfoque desde la perspectiva de la programación para dividir las dependencias, mientras que el patrón Repository lo hace desde la perspectiva de la arquitectura de software.
Incorporar el patrón Repository en el diseño arquitectónico proporciona flexibilidad para reemplazar la capa de acceso a datos. El éxito de un sistema depende no solo de cumplir con los requisitos funcionales del cliente, sino también de satisfacer requisitos no funcionales como la mantenibilidad y la flexibilidad. Un pequeño esfuerzo adicional durante el diseño beneficiara enormemente a los desarrolladores que posteriormente deban mantener el sistema.