Cuando desarrollamos aplicaciones web podemos caer en el error de dar demasiada responsabilidad a nuestros controladores lo que nos puede traer problemas en el futuro al intentar refactorizar ese código.
En este artículo veremos cómo partiendo de una acción donde la carga de datos se realiza desde el propio controlador podemos reducir el acoplamiento a la manera de guardar los datos. Con ello conseguiremos mejorar la reusabilidad y legibilidad del mismo, así como permitir realizar pruebas unitarias o cambiar componentes de manera sencilla.
Código inicial
Nuestro primer código es un simple controlador con un método que lista un catálogo de libros. Este controlador contiene un método que lista un catálogo de libros. Usaremos directamente sentencias SQL contra una base de datos LocalDB.
public class HomeController : Controller { public ActionResult Books() { List<Book> books = new List<Book>(); SqlConnection connection = new SqlConnection(@"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Books.mdf;Integrated Security=True"); SqlCommand cmd = new SqlCommand(); SqlDataReader reader; cmd.CommandText = "SELECT Title,Author FROM [Table]"; cmd.CommandType = CommandType.Text; cmd.Connection = connection; connection.Open(); reader = cmd.ExecuteReader(); if (reader.HasRows) { while (reader.Read()) { books.Add(new Book { Title = reader.GetString(0), Author = reader.GetString(1) }); } } connection.Close(); return View(books); } }
El resultado es una lista de objetos de tipo Book, que es un POCO (Plain Old C# Object) que contiene dos campos, Title y Author. La principal desventaja, y es la que intentaremos resolver a lo largo de este artículo, es que presenta un fuerte acoplamiento con el gestor de base de datos, y si cambiamos a MySQL o MongoDB, por poner un ejemplo, nos veríamos obligados a reescribir la esta implementación.
Primera aproximación, patrón Repository
El objetivo de un patrón Repository es crear una capa de abstracción entre la lógica de negocio (representada en este caso por el controlador) y la lógica de base de datos (representada por el acceso a SQL). Dicha capa de abstracción nos permite probar nuestro controlador de manera independiente a los datos y a la manera de almacenar los mismos.
La implementación de un patrón repository la podemos realizar usando una interfaz y su correspondiente implementación:
public interface IBookRepository { List<Book> GetAllBooks(); } public class BookRepository : IBookRepository { private SqlConnection _connection; private string _connectionString = @"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Books.mdf;Integrated Security=True"; public BookRepository() { _connection = new SqlConnection(_connectionString); } public List<Book> GetAllBooks() { List<Book> books = new List<Book>(); SqlCommand cmd = new SqlCommand(); SqlDataReader reader; cmd.CommandText = "SELECT Title,Author FROM [Table]"; cmd.CommandType = CommandType.Text; cmd.Connection = _connection; _connection.Open(); reader = cmd.ExecuteReader(); if (reader.HasRows) { while (reader.Read()) { books.Add(new Book { Title = reader.GetString(0), Author = reader.GetString(1) }); } } _connection.Close(); return books; } }
El código de la implementación es el mismo, ya que seguimos usando SQL para el acceso a datos, sin embargo hemos creado una abstracción que nos permite realizar pruebas unitarias y dejar los detalles de la implementación del acceso a datos al Repositorio.
El código del controlador se reduce al código siguiente:
public class HomeController : Controller { private IBookRepository _bookRepository; public HomeController() { _bookRepository = new BookRepository(); } public ActionResult Books() { return View(_bookRepository.GetAllBooks()); } }
Pese a que el repositorio se inicia desde el controlador, podríamos usar una solución de inyección de dependencias como Ninject o crear nuestra propia solución para que la inicialización de las dependencias se realice fuera del mismo.
Reduciendo acoplamiento en nuestro Repositorio
Una vez tenemos separados el controlador y el repositorio, tenemos varias alternativas para mejorar la legibilidad y la facilidad de uso del código
LINQ to SQL
Usar LINQ to SQL nos permite realizar un mapeado directo entre nuestra tabla y una clase, así como una capa de abstracción adicional sobre nuestro repositorio.
public class LinqToSqlBookRepository : IBookRepository { private DataContext _db; private string _connectionString = @"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Books.mdf;Integrated Security=True"; public LinqToSqlBookRepository() { _db = new DataContext(_connectionString); } public List<Book> GetAllBooks() { Table<Book> book = _db.GetTable<Book>(); return book.ToList<Book>(); } }
Para que el mapping funcione correctamente, hemos de decorar nuestro modelo con los nombres de las columnas equivalentes:
[Table(Name = "Table")] public class Book { [Column(Name = "Title")] public string Title { get; set; } [Column(Name = "Author")] public string Author { get; set; } }
Esta aproximación nos da una capa de abstracción mayor, comprobación de tipos en tiempo de compilación, capacidad de debug, y facilidad de acceso, a costa de perder algo de flexibilidad y rendimiento.
Conclusiones y pasos adicionales
LINQ to SQL no es la única manera de agregar abstracciones a nuestros datos, también podemos usar sistemas más complejos como Entity Framework, NHibernate, o Massive, siendo este el último que he descubierto y al que espero dedicarle pronto un artículo. Cada capa de abstracción, como decíamos anteriormente, nos permite desacoplar el código que estamos usando, aumentar su facilidad para ser probado, y su legibilidad, a la vez que cumplimos con principios como el de responsabilidad simple.
El proyecto termina con 3 capas diferenciadas:
- Capa de presentación (Controlador)
- Capa de datos (Repositorio)
- Capa de persistencia (SQL)
Podemos agregar capas de lógica de negocio (si queremos realizar validaciones sobre los datos antes de que lleguen a la base de datos) o de servicio (si tenemos varias interfaces para nuestra aplicación). Cada capa aumenta la complejidad de nuestra aplicación pero también aumenta nuestra capacidad para limitar las responsabilidades de cada clase.
Puedes ver el código completo en github
Lecturas adicionales
- Creating and executing SQL Statements
- Retrieving Data using a DataReader
- The Repository Pattern
- Simple Object Model and Query
Extra: Código de creación de la base de datos y datos de ejemplo
El código de creación de la base de datos y de los datos de ejemplo también es muy simple (SQL)
CREATE TABLE [dbo].[Table] ( [Id] INT NOT NULL PRIMARY KEY IDENTITY, [Title] NVARCHAR(50) NULL, [Author] NVARCHAR(50) NULL ) INSERT INTO [dbo].[Table] (Title,Author) VALUES ('Hamlet', 'William Shakespeare') INSERT INTO [dbo].[Table] (Title,Author) VALUES ('El Ingenioso Hidalgo Don Quijote de la Mancha', 'Miguel de Cervantes y Saavedra') INSERT INTO [dbo].[Table] (Title,Author) VALUES ('La divina comedia', 'Dante Alighieri')
Que tal Roberto? a ver, por donde empezamos…
1º- El repositorio no es solo una abstracion, el principio fundamental es «sensación de orientación a objetos, como de si una colección se tratara», deberías revisar este patrón en el libro de fowler por ejemplo.
2º- Si usas el patrón repositorio puedes hacerlo de varias formas ( amen de mal o bien), como si fuera un dao, que es lo que suena aquí, o empezar a hablar de responsabilidades, agregados, etc, en este ultimo caso, las cosas se pueden complicar un poco mas…
3º- Si utilizas L2S o EF o NH, en realidad para consultas, no esta tan claro que estas abstracciones aporten algo, no es un DataContext o un Dbcontext una forma de obtener repositorios = tus queryables? Hace tiempo en mi blog hable de esto y hay varios pros contras que quizás hubiera estado bien mencionarlos aquí..
4º- Utilizar atributos para el mapeo de tus clases es una forma de acoplamiento, que igual no te importa, pero lo es..
Seguro que hay mas cosas, pero un vistazo rápido es lo que me sale
Hola Unai, que honor el poder tener un comentario tuyo en mi humilde blog!
1. Le echaré un vistazo a los detalles del patrón del libro de fowler (supongo que te referirás a Patterns of Enterprise Application Architecture http://martinfowler.com/books/eaa.html).
2. Para la implementación del repository, me he basado en http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application (sin Unit of Work) ya que lo que quería era solamente la idea de la abstracción.
3. Aunque usemos un ORM, sigo viendo útil contar una abstracción intermedia, (llamémosla repository o N), para que sea más sencillo realizar la separación de responsabilidades y poder hacer pruebas. He estado buscando en tu blog y no he encontrado el artículo que mencionas, podrías agregarlo aquí?
4. Completamente de acuerdo, es acoplamiento.
Cual sería tu recomendación para separar la lógica de base de datos del controlador, ya sea con una implementación completa del patrón Repository u otro patrón diferente.
– Justificaciones a tu punto 3??? (leete http://geeks.ms/blogs/unai/archive/2011/07/26/idbset-iobjectset-como-repositorios-o-tus-propias-abstracciones.aspx) pero yo he ido cambiando de ideas con el tiempo, pero es bueno que tu lo pienses..
Gracias, le echaré un vistazo