Introducción a Contenedor de Inversión de Control (IOC Container)

jueves, 18 de septiembre de 2014


En anteriores post vimos una introducción a inyección de dependencias y introducción a inversión de control , en este vamos a ver como podemos realizar inyección de dependencias utilizando un contenedor de inversión de control.

¿Que es un contenedor de Inversión de Control?

Es un framework que se va a encargar de satisfacer las dependencias que tiene cada componente de nuestro código, gestionando de esta forma el ciclo de vida de nuestros objetos.

El proceso es el siguiente, en un punto de nuestra aplicación debemos registrar nuestros componentes (clases o interfaces), indicando los objetos que debe crear el contenedor cuando se necesiten.

Entonces si tenemos una clase que tiene dos dependencias que se le deben pasar en el constructor, cuando el contenedor resuelva la clase, el propio contenedor se va a encargar de instanciar nuestra clase y sus dependencias. De esta forma se produce la inversión de control, porque es el contenedor el que va a crear nuestros objetos y dependencias. Al crear nuestro objeto le inyectará las dependencias.

Debido a que el contenedor lleva el flujo de creación de los componentes según las dependencias e invoca nuestro código creando los objetos necesarios y no nuestro código el que le pregunta a él. Bueno en realidad le pregunta para el componente raiz y luego es cuando se desencadena la inversión de control y por este motivo es considerado un framework en lugar de una libreria.

Ventajas que nos aporta

Como ya hemos comentado va a gestinar el ciclo de vida de nuestros objetos, el se va a encargar de instanciarlos cuando es necesario porque otro objeto lo requiere.

Tenemos la posibilidad de reutilizar, si asi los configuramos, objetos entre contextos. Por ejemplo se puede registrar un componente como singleton, es decir, que siempre el contenedor devuelva el mismo objeto, podemos configurar que devuelva uno nuevo por petición web o incluso que devuelva uno nuevo siempre.

Otro punto a favor de los contenedores ioc es que si registras un componente para en el contexto de una petición web por ejemplo, cuando la petición web finaliza, el contenedor se va a encargar de liberar los recursos de los componentes dependientes de esa petición web. De esta forma por ejemplo si utilizamos un repositorio en el grafo de dependencias de la petición web, teniendo bien configurado el repositorio, el contenedor se encargará de invocar el metodo dispose donde nosotros cerramos la conexión con el servidor sql.

Hay contenedores donde se puede utilizar para el registro de componentes por convección, la convención más común es que si tienes un interface IProductService, la clase a utilizar para crear el objeto sera ProductService, de esta forma se elimina tambíen la necesidad de registrar componente a componente.

Se reduce considerablemenete la cantidad de código porque no vamos a tener que crearnos cada objeto en cada punto de la aplicación pasandole las dependencias que necesita.

Inconvenientes que tiene

Se pierde la seguridad que tenemos al compilar nuestro código, ya que si añadimos una dependencia nueva por parámetro a una clase y esta nueva dependencia no esta registrada, tendremos un error en tiempo de ejecución y no en tiempo de compilación.

También puede haber personas que se pueden no sentrir cómodas con la sensación de magia que se produce cuando los componentes se crean de forma automática segun las necesidades, dando asi la sencación de perdida de control.

Si tu aplicación es sencilla y con pocas dependencias es posible que estes añadiendole complejidad de forma innecesaria.

Cuando deberíamos usarlo

Bueno pues pienso que deberíamos utilizarlos cuando tengamos la estructura adecuada en una aplicación, porque un contenedor ioc asume que nuestros objetos van a depender de abstracciones y no de componentes concretos.

Dependerá también del número de dependencias que existan, es una aplicación sencilla puede que no sea lo mas adecuado pero si ya estamos hablando de algo más sério pienso que es buena inversión, porque te despreocupas de la creación de dependencias, solo te encargas de crear tus clases y registrar tus componentes en un punto concreto de tu aplicación.

Ejemplo

Para el ejemplo voy a utilizar Castle Windsor, no porque lo considere la mejor opción sino porque tengo que elegir uno. El análisis de contenedores IOC se escapa del alcance de este artículo.

Vamos a ver un mismo ejemplo con formas diferente de registrar de los componentes. El ejemplo va a ser una aplicación web Asp.net MVC, vamos a tener un modelo.

public class Product
{
    public int ProductId { get; set; }
    public string ImageUrl { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
}


una interfaz de repositorio y su implementación

public interface IProductRepository
{
    IEnumerable Get();
}

public class ProductRepository:IProductRepository, IDisposable
{
    ProductContext context = null;

    public ProductRepository()
    {
        context = new ProductContext();
    }

    public IEnumerable Get()
    {
        return context.Products;
    }

    public void Dispose()
    {
        if (context != null)
        {
            context.Database.Connection.Close();
            context.Dispose();
        }
    }
}


Tenemos un logger, que para nuestro ejemplo escribirá en la ventana output.

public interface ILogger
{
    void Info(string message);
}

public class Logger:ILogger
{
    public void Info(string message)
    {
        Debug.WriteLine(message);
    }
}


y vamos a tener un controlador que va a tener como dependencias el repositorio y el logger, ambos se van a pasar por parámetro al constructor.

public class HomeController : Controller
{
    private IProductRepository productRepostiory;
    private ILogger logger;

    public HomeController(IProductRepository productRepostiory, ILogger logger)
    {
        this.productRepostiory = productRepostiory;
        this.logger = logger;
    }

    public ActionResult Index()
    {
        var products = this.productRepostiory.Get();

        logger.Info("Products retrieved");

        return View(products);
    }
}


Puede que alguién no este de acuerdo con pasar un logger por parámetro y piense que lo más adecuado sería utilizar Programación Orientada a Aspectos (AOP) para este fin, pero mi intención es que el ejemplo se tome como un ejemplo práctico de uso de un contenedor IOC y no como una forma absoluta de hacer las cosas.

Vamos a registrar componente a componente, existe la posibilidad de no tener que hacerlo y registrar por convección. De esta forma podriamos indicar al controlador que escane este assembly, este assembly y registra todo lo que pertenezca a este namespace o que herede de esta clase. Prefiero registrar componente a componente para que quede el ejemplo más sencillo en cuanto a explicación.

Vamos a registrar nuestros componentes, para hacerlo nos tenemos que crear una clase que herede de IWindsorInstaller y en el método install registramos los componentes. Por defecto se registran con singleton, es decir, siempre nos devuelve la misma instancia. Para nuestro ejemplo tenemos que indicar que el controlador se registro por petición web porque sino asp.net nos dará un error diciendo que no se puede usar el mismo controlador en dos peticiones diferentes. Para el repositorio lo hacemos de la misma forma para poder liberar recursos cuando finalize la petición.

public class WindsorInstaller:IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Component.For().LifeStyle.PerWebRequest);
        container.Register(Component.For().ImplementedBy().LifeStyle.PerWebRequest);
        container.Register(Component.For().ImplementedBy()); 
    }
}


En el Global.asax nos creamos el contenedor y ejecutamos la instalación

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    var container = new WindsorContainer();
    container.Install(new WindsorInstaller());
}


Hasta aquí tenemos el código para registrar nuestros componentes pero falta enganchar esto con Asp.net MVC. En este tipo de proyectos existe una factoria por defecto de controladores, lo que tenemos que hacer es crearnos una personalizada que utilice el contenedor Castle Windsor para resolver los controladores y luego el contenedor se va a ha encargar de resolver las dependencias de todo el grafo.

Nos cremaos la factoría de controladores.

public class WindsorControllerFactory : DefaultControllerFactory
{
    private readonly IWindsorContainer container;

    public WindsorControllerFactory(IWindsorContainer container)
    {
        if (container == null)
        {
            throw new ArgumentNullException("container");
        }

        this.container = container;
    }

    protected override IController GetControllerInstance(
        RequestContext requestContext, Type controllerType)
    {
        return (IController)this.container.Resolve(controllerType);
    }

    public override void ReleaseController(IController controller)
    {
        this.container.Release(controller);
    }
}


En el método GetControllerInstance es donde se le pedimos al contenedor el controlador y este resuelve las dependencias de todo el grafo. El método release es invocado cuando la petición web termina, entonces invocamos el método release del contenedor, lo que va a realizar este es liberar los recursos para todos los componentes registrador para cada petición web. Por ejemplo si hubieramos dejado el ciclo de vida por defecto al registrar el repositorio, se hubiera registrado como singleton y aqui no se liberaria el contexto de Entity Framework.

Por último le tenemos que indicar en el Global.asax a Asp.net MVC que utilice nuestra factoría de controladores.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    //Resolve dependencies
    var container = new WindsorContainer();
    container.Install(new WindsorInstaller());

    ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(container));
}


Resumen

Hemos visto en este post como podemos realizar inyección de dependencias utilizando un contenedor de inversión de control. Hemos repasado ventajas e inconvenientes y también desde mi punto de vista cuando deberíamos usarlo.

Por último hemos visto un ejemplo utilizando Castle Windsor como contenedor de inversión de control.

Libros relacionados

Dependency Injection in .NET

No hay comentarios:

Publicar un comentario