Introducción a IoC y DI: Un ejemplo web

Sigo posteando material de lo que se está produciendo en el Proyecto Hogwarts. Esta vez, sobre el tema de Inversion of Control y Dependency Injection. Dentro de poco, estará disponible un sitio en línea con un curso de TDD, y otros en construcción. En el anterior post:

Introducción a IoC, DI: Hello, world flexible

había planteado un ejemplo simple, veamos alguno apenas más complejo. Y veamos de plantearla con interfaces.

Lo que vimos en ese ejemplo es que:

- Un objeto A puede necesitar la ayuda de otro objeto B, pero conociendo la interface, sin ligarse a una clase en concreto

- Al objeto A alguien le provee el objeto B

Lo que vamos explorar ahora, es un ejemplo donde al objeto A le damos alguna vez un objeto B1, y otras veces el objeto B2. Para que no sea sólo una aplicación de consola, veamos de levantar el alcance, y hagamos un ejemplo web.

Sea una simple aplicación web, donde tenemos una página para presionar dos enlaces para leer y mostrar los clientes:

La idea es tener DOS implementaciones de una interface que se haga responsable de implementar un repositorio de clientes (digamos, una lista de clientes). Si presionamos en el primer enlace, se usará un repositorio en memoria:

 

Si presionamos en el segundo enlace, usaremos el repositorio de base de datos:

Pueden bajarse el ejemplo desde mi Skydrive CustomersExample.zip. Contiene una solución .NET y los scripts para crear la base de datos en SQL Server.

En este ejemplo, vamos a usar un servicio que se ayudará de un repositorio para conseguir los objetos que necesita la presentación. Veamos cómo se implementó.

Tenemos entonces, un servicio que necesita consumir un repositorio. Como en el ejemplo de Hello, world, el objeto consumidor no referencia a una clase concreta, sino a una interfaz:

Esa interfaz está definida como:

namespace Customers
{
 public interface ICustomerRepository
 {
 Customer GetById(int id);
 void Insert(Customer customer);
 void Update(Customer customer);
 void Delete(Customer customer);
 IQueryable<Customer> GetAll();
 }
}


Luego tenemos dos implementaciones de esta interfaz:





Una es la implementación del repositorio en memoria, parte del código:



 public class InMemoryCustomerRepository : ICustomerRepository
 {
 private IList<Customer> customers = new List<Customer>();
 public InMemoryCustomerRepository()
 {
 for (int k = 1; k <= 10; k++)
 {
 customers.Add(new Customer()
 {
 Id = k,
 Name = string.Format("Customer {0}", k),
 Address = string.Format("Address {0}", k),
 Notes = string.Format("Notes {0}", k)
 });
 }
 }
//....
 }


La otra implementación de la interfaz accede a una base de datos, código parcial:



 public class DatabaseCustomerRepository : ICustomerRepository
 {
 private string connectionstring;
 public DatabaseCustomerRepository(string connectionstring)
 {
 this.connectionstring = connectionstring;
 }
 public Customer GetById(int id)
 {
 SqlConnection conn = new SqlConnection(this.connectionstring);
 SqlCommand cmd = new SqlCommand("select Id, Name, Address, Notes from Customer where Id = @Id", conn);
 cmd.Parameters.Add(new SqlParameter("@Id", id));
 conn.Open();
 SqlDataReader reader = cmd.ExecuteReader();
 Customer customer = null;
 if (reader.Read())
 customer = this.GetCustomer(reader);
 reader.Close();
 conn.Close();
 return customer;
 }
///...
 }


Lo que se usó en este ejemplo es que al crear el objeto servicio le pasamos en los argumentos del constructor cuál implementación de repositorio queremos usar. Así, en la acción de ver por memoria, el código es:



protected void lnkMemory_Click(object sender, EventArgs e)
 {
 CustomerService service = new CustomerService(new InMemoryCustomerRepository());
 this.grdCustomers.DataSource = service.GetCustomers();
 this.grdCustomers.DataBind();
 }


La otra opción es por base de datos:



 protected void lnkData_Click(object sender, EventArgs e)
 {
 CustomerService service = new CustomerService(new DatabaseCustomerRepository("server=.\SQLEXPRESS;database=Customers;integrated security=true"));
 this.grdCustomers.DataSource = service.GetCustomers();
 this.grdCustomers.DataBind();
 }


Notemos que al servicio le "inyectamos" su ayudante, esta vez usando un parámetro de su constructor (en el ejemplo Hello World, habiamos inyectado usando propiedades).



Esto nos muestra:



- La capacidad de abstraer lo que necesitamos (la interfaz de repositorio) de cómo lo implementamos
- Nos permite cambiar la implementación y que todo siga funcionando igual



Acá usamos las DOS implementaciones al mismo tiempo. Pero es más común usar solo una.



Podríamos haber codificado el acceso a los clientes en el propio servicio. Pero lo separamos para:



- Poder mejorar el servicio (p.ej., en temas de seguridad) separando esas otras funcionalidades de la más simple de recuperación de clientes.



- Poder cambiar el ayudante, sin necesidad de cambiar el servicio y sus consumidores.



Hasta podemos ir armando una aplicación de demostración, usando durante semanas la implementación del repositorio en memoria. Mientras, en un desarrollo ágil, vamos iterando, mejorando nuestro dominio, SIN tener una base de datos por debajo. El cliente final va viendo nuestro avance, y solamente cuando tenemos algo más definido, vamos definiendo la base de datos subyacente y operando sobre ella.



También, este "approach" nos permite cambiar la implementación de la base de datos. Ahora, la implementación va contra SQL Server, pero si mañana necesitamos ir contra Oracle, cambiamos la implementación, y ni servicio y otros se enteran de ese cambio.



Y por último, veremos, más adelante en otro curso (Mocks con TDD), que podemos usar implementaciones de repositorios que no vayan contra la base, para alivianar nuestros tests del servicio.



Y todo, por haber programado contra la interfaz, y segregado responsabilidades.



Tenemos que avanzar en un sentido: no queremos tener que cambiar el código de creación del servicio, para que una vez use una implementación del repositorio (en memoria) y otra vez use una distinta (en base de datos). Quisiéramos que el cambiar esa relación NO IMPLIQUE tener que cambiar el código. Esa es la idea que subyace en lo que tenemos que definir en los próximos post de este tema: qué es Inversion of Control, qué es Dependency Injection, y contenedores de IoC.





Nos leemos!



Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

This entry was posted in 13620, 1389, 14126, 14127, 5374. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>