patron-repository

Patrón Repository: abstrae el acceso a datos

  • 5 min

Un Repository es una abstracción que ofrece operaciones de acceso a entidades del dominio sin exponer al consumidor los detalles de persistencia. Se parece al mostrador de una biblioteca: pides un libro sin tener que conocer su ubicación física.

Imaginad que vais a una biblioteca a por un libro. No bajáis al sótano, abrís las cajas de embalaje y buscáis el libro vosotros mismos. Lo que hacéis es ir al mostrador y preguntarle al bibliotecario: “Hola, ¿tienes ‘El Quijote’?”. El bibliotecario va, lo busca (sabe si está en la estantería 5 o en el almacén), y os lo trae. A vosotros os da igual de dónde lo ha sacado.

El patrón Repository actúa como ese bibliotecario.

Es una capa de abstracción entre la Lógica de Negocio de nuestra aplicación y la Capa de Acceso a Datos. Su objetivo es hacer que el acceso a la base de datos se parezca a trabajar con una simple colección de objetos en memoria.

El Problema: SQL Espagueti

Sin este patrón, es común ver código donde los Controladores o Servicios saben demasiado sobre la base de datos.

// ❌ Código acoplado y difícil de testear
public class UsuarioService
{
    public void RegistrarUsuario(Usuario user)
    {
        // Lógica de negocio mezclada con acceso a datos
        if (user.Edad < 18) throw new Exception("Menor de edad");

        // ¡Acoplado a SQL Server y a la librería SqlClient!
        using (var conn = new SqlConnection("Server=..."))
        {
            conn.Open();
            var cmd = new SqlCommand("INSERT INTO Users VALUES (@name)...", conn);
            cmd.ExecuteNonQuery();
        }
    }
}
Copied!

Problemas:

  1. Duplicidad: Si tienes que buscar usuarios en varios sitios, repetirás la query SELECT * FROM....
  2. Testabilidad nula: Para probar RegistrarUsuario, necesitas una base de datos real instalada y corriendo.
  3. Rigidez: Si quieres cambiar de SQL Server a MongoDB o a un archivo XML, tienes que reescribir toda la aplicación.

La Solución: El Repositorio

El patrón Repository propone crear una Interfaz que defina las operaciones de acceso a datos (CRUD: Create, Read, Update, Delete) usando objetos de dominio, no tablas ni SQL.

La lógica de negocio solo hablará con esta interfaz.

Implementación en C#

Vamos a desacoplar nuestro servicio de usuarios.

La Entidad (Modelo)

Un objeto POCO (Plain Old CLR Object) simple.

public class Usuario
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public string Email { get; set; }
}
Copied!

La Interfaz del Repositorio

Aquí definimos el contrato. Fíjate que no hay ni rastro de SQL.

public interface IUsuarioRepositorio
{
    Usuario GetPorId(int id);
    IEnumerable<Usuario> GetTodos();
    void Agregar(Usuario usuario);
    void Eliminar(int id);
}
Copied!

La Implementación Concreta (SQL)

Aquí es donde nos ensuciamos las manos con la base de datos. En un proyecto real usaríamos un ORM como Entity Framework Core, pero para el ejemplo simularemos una lista o acceso directo.

public class UsuarioRepositorioSQL : IUsuarioRepositorio
{
    // Simulamos la DB
    private static List<Usuario> _fakeDb = new List<Usuario>(); 

    public Usuario GetPorId(int id)
    {
        // Aquí iría: _context.Users.FirstOrDefault(u => u.Id == id);
        return _fakeDb.FirstOrDefault(u => u.Id == id);
    }

    public IEnumerable<Usuario> GetTodos()
    {
        return _fakeDb;
    }

    public void Agregar(Usuario usuario)
    {
        Console.WriteLine($"[SQL] Insertando usuario {usuario.Nombre} en tabla Users...");
        _fakeDb.Add(usuario);
    }

    public void Eliminar(int id)
    {
        var user = GetPorId(id);
        if (user != null) _fakeDb.Remove(user);
    }
}
Copied!

El Consumidor (Inyección de Dependencias)

Ahora nuestro servicio es limpio. Recibe el repositorio por inyección (DI).

public class UsuarioService
{
    private readonly IUsuarioRepositorio _repo;

    // Inyectamos la interfaz, no la clase concreta
    public UsuarioService(IUsuarioRepositorio repo)
    {
        _repo = repo;
    }

    public void RegistrarUsuario(string nombre, string email)
    {
        // Validaciones de negocio
        if (string.IsNullOrEmpty(nombre)) 
            throw new ArgumentException("El nombre es obligatorio");

        var nuevoUsuario = new Usuario { Id = new Random().Next(100), Nombre = nombre, Email = email };

        // Delegamos el guardado al repositorio
        _repo.Agregar(nuevoUsuario);
    }
    
    public void MostrarTodos()
    {
        var usuarios = _repo.GetTodos();
        foreach(var u in usuarios) 
            Console.WriteLine($"- {u.Nombre} ({u.Email})");
    }
}
Copied!

Puesta en marcha

class Program
{
    static void Main(string[] args)
    {
        // 1. Configuramos las dependencias (normalmente lo hace el contenedor DI)
        IUsuarioRepositorio repo = new UsuarioRepositorioSQL();
        var servicio = new UsuarioService(repo);

        // 2. Usamos el servicio
        servicio.RegistrarUsuario("Luis", "[email protected]");
        servicio.RegistrarUsuario("Ana", "[email protected]");

        Console.WriteLine("\nListado de usuarios:");
        servicio.MostrarTodos();
    }
}
Copied!

Repository genérico

Uno de los problemas de este patrón es que terminas creando muchas clases repetitivas (ProductoRepositorio, PedidoRepositorio, FacturaRepositorio) que hacen casi lo mismo.

En C# podemos usar Genéricos para crear un Repositorio Base.

public interface IRepositorio<T> where T : class
{
    T GetById(int id);
    void Add(T entity);
    // ...
}

public class RepositorioGenerico<T> : IRepositorio<T> where T : class
{
    // Implementación usando DbContext de Entity Framework
    // public T GetById(int id) => _dbSet.Find(id);
}
Copied!

Así podemos reducir código repetitivo, pero un repositorio genérico de CRUD puede limitar consultas útiles o duplicar la API del ORM. En EF Core, DbSet<T> ya ofrece buena parte de este comportamiento; añade una capa propia cuando exprese operaciones del dominio o cree un límite real.

Ventajas Clave

  1. Testabilidad: Puedes usar un repositorio de prueba para aislar reglas de negocio. Aun así, necesitas pruebas de integración porque una lista no reproduce consultas, restricciones ni transacciones de una base de datos real.
  2. Desacoplamiento: Puedes cambiar el motor de base de datos sin tocar la lógica de negocio.
  3. Centralización: Las consultas complejas (ej: “Usuarios activos con más de 5 pedidos”) están en un solo sitio, no esparcidas por 20 controladores.

Repository es una opción habitual para separar datos y lógica, pero no es obligatorio encima de cualquier ORM.