Eliminando acoplamiento en un controlador ASP.net MVC

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

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')

4 pensamientos en “Eliminando acoplamiento en un controlador ASP.net MVC

  1. uzorrilla

    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

    Responder
    1. Roberto Luis Bisbé Autor de la entrada

      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.

      Responder

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s