Patrón Repository

Hoy os traigo un patrón de diseño, en concreto el patrón Repository

Antes de explicar en qué consiste dicho patrón y de meternos en faena voy a explicar el por qué es útil.

Todos sabemos que nuestra aplicación ideal es aquella donde cada parte de la misma está bien separada de las otras, donde la mantenibilidad sea máxima, donde podamos ampliar por donde queramos sin bloquearnos en partes donde la arquitectura no puede crecer más, aquella que nos permita cambiar una parte de la aplicacion sin efectos secundarios…

Un mundo color de rosa, ¿Verdad? Sin duda.

¿Cuál es el problema? Pues que no podemos hacer algo en plan:

application.AplicacionIdealMode();

Necesitamos cuidar trozo de código que escribamos, debemos organizar bien las clases, desacoplarlas lo máximo posible… Se que es mucho, pero granito a granito hacemos la playa, quiero decir, la aplicación ideal.

Hoy vengo a contaros uno de esos granitos.

Como sabéis, algo que ayuda mucho es dividir la aplicación en capas, por ejemplo en una aplicación WPF pues lo ideal es la vista, el viewmodel, el modelo y… la lógica del negocio.

Lo mismo para una aplicación web, vista, controlador, modelo y la lógica del negocio.

Para este artículo en cuestión lo que nos interesa es la lógica del negocio.

Imaginad que tenéis 15 clases que contienen información que será almacenada en “lo que sea”, una base de datos, a un servidor mail, a un fichero de texto plano… Si hoy decides que todo eso va a texto plano y cada clase se encarga de guardar su información en un .txt, pues será una real gracia cuando mañana tu aplicación crezca y necesites una base de datos para que esto siga avanzando. ¿Qué harás? ¿Cambiar media aplicación para que deje de usar un .txt y ahora use una base de datos? ¿Qué pasa si pasado ya no quieres bases de datos? ¿Y si X clase necesita además enviar datos por email? Suena a mucho lio que te hará perder mucho tiempo.

¿Qué hacemos en este caso? Pues colocar todo lo que sea el acceso a datos, o sea, la persistencia en otro sitio y así desacoplar la aplicación lo máximo posible.

¿Empezamos?

Creamos una nueva solución vacía en Visual Studio (solución en blanco)

Le ponemos de nombre pues: TutoRepositorios

Ahi creamos una nueva libreria de clases llamada: TutoRepositorios.Dominio

Borramos el fichero Class1.cs y creamos un directorio llamado Entidades, dentro de dicho directorio creamos una clase llamada Libro, el código de dicha clase será:

namespace TutoRepositorios.Dominio.Entidades
{
    public class Libro
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public string Autor { get; set; }
    }

	public override string ToString()
	{
		return string.Format("Nombre: {0}nAutor: {1}", Nombre, Autor);
	}
}

Algo sencillito para nuestro ejemplo :) .

Vamos a crear una aplicación que muestre un listado de libros, para ello vamos a crear crear un repositorio para los libros.

Pero… ¿Qué es un repositorio? Un repositorio es una clase que servirá como intermediario entre nuestra aplicación y nuestros datos, dicho con otras palabras, será una clase que nos ofrecerá una interfaz CRUD (Crear, Obtener, Actualizar, Borrar (Create, Retrieve, Update, Delete en inglés)).

Vamos a implementarla para entenderlo mejor.

¿Cómo lo hacemos? ¿Creamos una clase llamada no sé LibroRepositorio y ahi metemos todo el código? Sí y no, si bien es una forma correcta de hacerlo, por muy poco esfuerzo más podemos añadir unos cuantos granos más a nuestra playa, errr aplicación.

Para ello vamos a crear una interfaz para nuestro repositorio, para ello creamos una nueva carpeta en nuestro dominio llamada: Repositorios, dentro creamos una interfaz llamada: ILibrosRepositorio

namespace TutoRepositorios.Dominio.Repositorios
{
    public interface ILibrosRepositorio
    {
        void Insertar(Libro libro);
        void Borrar(Libro libro);
        Libro Obtener(int id);
		IList<Libro> ObtenerTodos();
        void GuardarCambios();
    }
}

Una interfaz muy sencillita.

Ahora bien, ¿No es esto repetir código? Quiero decir, ¿Para qué una interfaz y luego una clase que la implemente? Fácil, si tienes un repositorio que usa un .txt para persistencia y luego quieres uno para guardar en bases de datos, pues ya tienes una interfaz que declara el cómo ha de ser un repositorio de libros, solo tienes que implementar dicha interfaz y ya está.

No solo eso, para hacer pruebas unitarias esto es un requerimiento.

Imagina que la clase que quieres testear usa el repositorio de libros el cual consulta a una base de datos. ¿No sería contraprudecente testear dicha clase y que ésta a la vez tenga que acceder a la base de datos? Imagina que para la prueba de dicha clase has de crear 10 elementos en el repositorio y luego borrar 3, eso se queda guardado en la base de datos… ¿La solución? Crear un repositorio de mentira para las pruebas, así podrás probar la clase sin depender de la base de datos, y no generarás basura en tus pruebas.

Uhm, interesante… Vamos a crear un repositorio de mentira a ver…

Crea una nueva clase llamada: FakeLibrosRepositorio en la carpeta de repositorios:

namespace TutoRepositorios.Dominio.Repositorios
{
    public class FakeLibrosRepositorio : ILibrosRepositorio
    {
        private IList<Libro> _libros = new List<Libro>();

        public void Insertar(Libro libro)
        {
            _libros.Add(libro);
        }

        public void Borrar(Libro libro)
        {
            _libros.Remove(libro);
        }

        public Libro Obtener(int id)
        {
            return _libros.SingleOrDefault(l => l.Id == id);
        }

        public IList<Libro> ObtenerTodos()
        {
            return _libros.ToList();
        }

        public void GuardarCambios()
        {
            // No necesitamos guardar nada en este repo fake.
        }
    }
}

Como veis, un repositorio muy básico donde realmente no persiste los datos, solo los almacena en una lista.

¿Lo probamos?

Añadimos un nuevo proyecto a nuestra solución, concretamente una aplicación de consola (Que para el caso nos da igual), vamos a llamarla: TutoRepositorios.Consola

Añadimos una referencia del proyecto TutoRpositorios.Dominio para poder usar nuestro repositorio, añadimos el siguiente código de prueba en la clase program.cs

namespace TutoRepositorios.Consola
{
    class Program
    {
        static void Main(string[] args)
        {
            // Creamos un repositorio fake
            ILibrosRepositorio repo = new FakeLibrosRepositorio();

            // Insertamos 3 libros
            repo.Insertar(new Libro {Id = 1, Autor = "Jesus", Nombre = "Operadores de Linq"});
            repo.Insertar(new Libro {Id = 2, Autor = "Alvaro", Nombre = "Php para todos"});
            repo.Insertar(new Libro {Id = 3, Autor = "Manolo", Nombre = "Java Avanzado"});

            // Vamos a impromir el libro con el Id == 1
            Console.WriteLine("Libro Id == 1");
            Console.WriteLine(repo.Obtener(1).ToString() + "n");

            // Ya no queremos el libro primero, vamos a borrarlo
            var libro = repo.Obtener(1);

            if (libro != null)
                repo.Borrar(libro);

            // Vamos a crear una lista con los libros de nuestro repo
            var libros = repo.ObtenerTodos();

            // Vamos a imprimirlos
            Console.WriteLine("Todos los libros:");
            foreach (var lib in libros)
                Console.WriteLine(lib.ToString() + "n");
        }
    }
}

Sencillo de usar nuestro repositorio, ¿No? Hemos añadido 3 libros, hemos imprimido uno, borrado otro… La interfaz de nuestro repositorio es muy muy clarita y eso nos facilita el trabajo.

He aquí el patrón repositorio, nada más.

Bueeno, os contaré más, y así verle más la gracia a esto de los repositorios.

Nuestro FakeLibrosRepositorio es un poco basura, ¿Verdad? Si bien funciona, no hay persistencia…

Vaaale vaaale, vamos a persistir los datos en un SQL Server. Para ello nos vamos al explorador de servidores -> Nueva conexión, seeleccinamos SQL Server, colocamos el nombre de nuestro servidor, en mi caso: .SQLEXPRESS le damos un nombre, por ejemplo: TutoRepositorio.

Ahora creamos una tabla de la siguiente manera:

Acordaros de añadir en la columna “Id” que sea identity, para ello os vais a las propiedades de la columna, buscás la parte de la especificación de identidad, lo expandís y le dais a que sí en lo de si es identidad (no sé muy bien como estará en castellano).

Vamos a añadir algunos datos de prueba:

Recordad que el Id se pone solo :)

Vale, ya tenemos la base de datos y algunos datos…

Como esto es un ejemplo, vamos por lo fácil, vamos a tirar de Linq To Sql y vamos a mapear manualmente nuestra clase Libro para que corresponda con la tabla de la base de datos. (En un futuro quiero enseñar como usar la técnica code-first para hacer esto más bonito)

La clase Libro quedaría así:

namespace TutoRepositorios.Dominio.Entidades
{
    [Table(Name = "Libros")]
    public class Libro
    {
        [Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync=AutoSync.OnInsert)]
        public int Id { get; set; }

        [Column] public string Nombre { get; set; }
        [Column] public string Autor { get; set; }

        public override string ToString()
        {
            return string.Format("Nombre: {0}nAutor: {1}", Nombre, Autor);
        }
    }
}

NOTA: Necesitais agregar una referencia de System.Data.Linq al proyecto para poder usar dichos atributos

Hecho, ya está eso listo.

Ahora nos toca el repositorio para esto. Como habéis deducido es tan sencillo como crear una clase que implemente nuestra interfaz y ya está. Creamos SqlLibrosRepositorio en la carpeta de repositorios:

namespace TutoRepositorios.Dominio.Repositorios
{
    public class SqlLibrosRepositorio : ILibrosRepositorio
    {
        private DataContext _db;
        private Table<Libro> _libros;

        public SqlLibrosRepositorio(string cadenaConexion)
        {
            _db = new DataContext(cadenaConexion);
            _libros = _db.GetTable<Libro>();
        }

        public void Insertar(Libro libro)
        {
            _libros.InsertOnSubmit(libro);
        }

        public void Borrar(Libro libro)
        {
            _libros.DeleteOnSubmit(libro);
        }

        public Libro Obtener(int id)
        {
            return _libros.SingleOrDefault(l => l.Id == id);
        }

        public IList<Libro> ObtenerTodos()
        {
            return _libros.ToList();
        }

        public void GuardarCambios()
        {
            _db.SubmitChanges();
        }
    }
}

¿Queda muy claro verdad? Te preguntarás el por qué el constructor del repositorio recibe la cadena de conexión en vez de asignarsela directamente dentro del repositorio. Es otro granito de arena más, si injectamos la cadena de conexión en el constructor, podremos reusar este repositorio con cualquier base de datos que queramos :)

Vale, vamos ahora a usar este repo en nuestra aplicación de consola:

namespace TutoRepositorios.Consola
{
    class Program
    {
        static void Main(string[] args)
        {
            // Creamos un repositorio
            ILibrosRepositorio repo = new SqlLibrosRepositorio(@"Data Source=.SQLEXPRESS;Initial Catalog=TutoRepositorio;Integrated Security=True");

            // Vamos a impromir el libro con el Id == 1
            Console.WriteLine("Libro Id == 1");
            Console.WriteLine(repo.Obtener(1).ToString() + "n");

            // Ya no queremos el libro primero, vamos a borrarlo
            var libro = repo.Obtener(1);

            if (libro != null)
                repo.Borrar(libro);

            // Vamos a crear un nuevo libro
            libro = new Libro {Autor = "Pco", Nombre = "Patrones de diseño"};
            repo.Insertar(libro);

            // Ouch, puse Pco en vez de Paco, vamos a actualizarlo
            libro.Autor = "Paco";

            repo.GuardarCambios();

            // Vamos a crear una lista con los libros de nuestro repo
            var libros = repo.ObtenerTodos();

            // Vamos a imprimirlos
            Console.WriteLine("Todos los libros:");
            foreach (var lib in libros)
                Console.WriteLine(lib.ToString() + "n");
        }
    }
}

Como veis, la forma de usar el repositorio no ha cambiado en absoluto, insertamos y obtenemos libros de la misma forma, usando la misma interfaz. La unica diferencia es que ahora tenemos la posibilidad de persistir nuestros datos :)

Otra ventaja de esto es que si ahora queremos usar este dominio en no sé, una aplicación ASP.NET MVC pues no tendríamos que cambiar nada, solo usar el repositorio como hemos hecho aquí.

No voy a detallar los pasos, mostraré lo relevante y el resultado:

El controlador que con la acción de mostrar los datos:

namespace TutoRepositorios.WebUi.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        private ILibrosRepositorio _repo =
            new SqlLibrosRepositorio(
                @"Data Source=.SQLEXPRESS;Initial Catalog=TutoRepositorio;Integrated Security=True");

        public ActionResult Index()
        {
            var libros = _repo.ObtenerTodos();
            return View(libros);
        }

    }
}

Simplemente creamos el repo, creamos una lista de libros y la enviamos a la vista que la imprime:

¿Habeis visto? ¡Hemos creado una capa de acceso a datos independiente del resto, la hemos usado en una aplicación de consola, en una web y no hay que cambiarla!

Consideraciones finales

El método de guardar los cambios… Es lógico que tenemos que guardar nuestros datos pero… ¿No es un poco pesado tener que ir llamando al dichoso método cada vez que hagamos cambios?

Tenemos dos opciones y las dos perfectamente válidas:

Crear un método que llamaremos explicitamente cuando lo necesitemos (Como hemos hecho aquí). La ventaja que tiene es que nos permite hacer varios cambios al repositorio sin guardar los cambios, lo que nos da flexibilidad, por contra es eso, tenemos que ir llamandolo muchas veces.

No crearlo, guardar implicitamente los cambios en cada método del repositorio que haga cambios a la base de datos. Esto es útil si realmente necesitamos guardar los cambios cada vez que toquemos algo, así nos ahorramos código :)

Todavía hay más granitos de arena, pero esos ya se salen fuera de este tutorial.

Por ejemplo, todavía se puede desacoplar lo que hemos visto algo más. Echandole un ojo al controlador de este último ejemplo… Estamos instanciando el repositorio ahi a mano, lo que nos quita flexibilidad…. Si queremos testear dicho controlador, nos vamos a tener que tragar ese repositorio, y si queremos cambiarlo por uno Fake como el que ya tenemos, necesitariamos reasignar el repositorio al Fake y bueno, eso no es flexible para nada…

¿La solución? Injección de dependencias, ¿La explicación? En otro artículo :)

Ahora sí que está esto terminado.

Os dejo el proyecto aquí (.NET 4)

Hasta otra.

Tags: ,

10 comentarios

  1. Excelente aporte, para ser mas completo le agregaria las referencias para poder usar los datos, me refiero en este caso desde libros a la interface del repositorio.

    Saludos.,

  2. Hola Cristian, me gustaría que explicaras mejor tu sugerencia para discutirla :P

  3. Gracias por la explicación acerca de este patrón.

    Tengo una duda haber si me podes sugerir algo, con el patrón MVVM, con respecto a la capa de Model es aconsejable utilizar algo como lo utilizaste en TutoRepositorios.Dominio.Entidades? o los Model deberían de ser POCOS?

  4. Hombre, viendo lo que he escrito ahí, era por ir más a lo fácil, tirar de una base de datos rápida.

    Puedes usar la opción que quieras (Generar las clases usando un edmx desde la base de datos, que para mi gusto, ya no se usa).

    Hacer la base de datos (sql server, compact….) y luego crear tu un POCO (o los que hagan falta) y mapear tus tablas a dichos POCOs (Lo que se llama mapear una base de datos existente) o bien usar CodeFirst (del que tengo un artículo en el blog también) y básicamente creas el POCO y eso te generará la base de datos (dicho POCO lo puedes personalizar usando anotaciones, pero seguiría siendo un POCO).

    Espero haberte solucionado la duda.

  5. Una de las mejores guías rápidas que he leído. Muchas gracias.

  6. muy buen tuto, pero podrias resubir el codigo porfavor

  7. Hecho, escribí mal la ruta del fichero :P

Dejar un comentario