Inyección de dependecias e Inversión de control

La verdad, no soy consciente de cuántos son los desarrolladores que conocen el significado de Inyección de Dependencias (Dependency Injection) o Inversión de control (Inversion of Control). Para ser sincera, nunca trabajé con ello en ningún proyecto real pero si he intentado recopilar información y conceptos para poder aplicarlos a mis proyectos personales.

Generalmente, cuando tenemos una clase que depende de otras para ciertas acciones, necesitamos inicializar instancias de las mismas para poder utilizarlas posteriormente. En ocasiones, una de las alternativas puede ser crear un objeto de dichas clases de forma privada e inicializarlas, utilizando el constructor de la clase principal.

Si vemos estas acciones desde el punto de vista de la Inyección de Dependencias y la Inversión de Control, no sería la forma más óptima debido a que la clase que sufre estas dependencias no debería ser la responsable de la creación de las mismas.

¿QUÉ CONSEGUIMOS?

  • Desacoplamiento.
  • Mejora la posibilidad de testeo de la aplicación.
  • Mejor mantenimiento a la hora de realizar cambios de los componentes, gracias a la modularidad.
  • […]

Ambos conceptos están tan ligados que, en ocasiones, no se hace distinción. Se utiliza el concepto Inversión de Control para delegar en otro componente, un framework por ejemplo, la responsabilidad de crear las instancias necesarias en lugar de crearlas nosotros mismos. Por otro lado, la Inyección de Dependencias es el término utilizado cuando una clase depende de otra y, a través del constructor generalmente acepta un parámetro del tipo del cual depende. 

Para llevar a cabo el patrón de diseño de Inyección de Dependencias, es necesario el uso de interfaces y, lo más óptimo sería utilizar alguno de los frameworks disponibles para llevar a cabo la Inversión de Control. Algunos de estos frameworks son: Spring.NETWindsor Container, StructureMap, Unity, etcétera.

EJEMPLO A EVITAR

Por ver un ejemplo, supongamos que tenemos el siguiente código:

using System.Web.Mvc;
using IoC.Models;

namespace IoC.Controllers
{
[HandleError]
public class HomeController : Controller
{
private readonly ITwitterService _twitterService;
public HomeController()
{
_twitterService = new TwitterService();
}

public ActionResult Index()
{
return View(_twitterService.FetchTweets());
}
}
}

Tenemos un controlador, en una aplicación ASP.NET MVC, donde estamos haciendo uso de una librería que conecta con Twitter. Cuando se solicita la acción Index de este controlador, el controlador se crea, a través del constructor inicializa la variable _twitterService y realiza la llamada a FetchTweets. Esto funciona sin problemas, pero supone un inconveniente a la hora de realizar pruebas unitarias.

Por otro lado, si el día de mañana queremos, por ejemplo, utilizar otra clase que implemente ITwitterService bien porque hemos cambiado de librería, porque la forma de trabajar con Twitter es totalmente distinta, etcétera, deberíamos modificar a su vez este controlador(es) para modificar en el constructor la clase que implementa la interfaz a partir de ahora. Este es un caso bien simple pero ¿Y si nuestros controladores son dependientes de más de una clase y las mismas están en constante revisión, actualización, modificación, etcétera? La solución es bien simple:

using System.Web.Mvc;
using IoC.Models;

namespace IoC.Controllers
{
[HandleError]
public class HomeController : Controller
{
private readonly ITwitterService _twitterService;
public HomeController(ITwitterService twitterService)
{
_twitterService = twitterService;
}

public ActionResult Index()
{
return View(_twitterService.FetchTweets());
}
}
}

Gracias a la inyección de dependencias, liberamos al controlador de la carga de generar las instancias que necesita, lo abstraemos del tipo de clase que implementa la interfaz en este momento y conseguimos modularizar la aplicación.

PRUEBAS UNITARIAS

Si quisiéramos hacer un test, por ejemplo, que comprobara que al llamar a la acción Index el método FetchTweets es llamado, sin realizar la llamada real a Twitter e incluso sin hacer uso de la conexión que esto requiere a Internet, podríamos hacerlo de la siguiente manera:

using IoC.Controllers;
using IoC.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;

namespace IoC.Tests.Controllers
{
[TestClass]
public class HomeControllerTest
{
[TestMethod]
public void HomeController_AlLlamarALaAccionIndex_FetchTweetsEsLlamado()
{
//Arrange
var mockTwitterService = MockRepository.GenerateMock<TwitterService>();
mockTwitterService.Stub(m => m.FetchTweets()).Return(null);
var homeController = new HomeController(mockTwitterService);

//Act
homeController.Index();

//Assert
mockTwitterService.AssertWasCalled(m => m.FetchTweets());
}
}
}

Nota: En este ejemplo he utilizado la librería RhinoMocks para crear la prueba unitaria.

Esto es realmente importante si queremos hacer pruebas unitarias de la aplicación, pero también es cierto que se nos presenta el siguiente problema ¿Cada vez que llame a HomeController voy a tener que encargarme y asegurarme en cada caso de que reciba una instancia de las interfaces que solicita el constructor? Aquí es donde entra en juego IoC y los numerosos frameworks existentes para este rol.

Para ver un pequeño ejemplo de cómo podríamos delegar esta funcionalidad en uno de los frameworks que soportan Inversión del Control, voy a utilizar StructureMap como demostración. Para descargar la última versión del framework podemos dirigirnos a este enlace.

CONFIGURANDO STRUCTUREMAP

En este ejemplo, vamos a configurar StructureMap de tal forma que sepa cómo actuar en el caso de requerir una instancia para una interfaz de tipo ITwitterService. Para ello, me he creado la siguiente clase:

using IoC.Models;
using StructureMap;

namespace IoC.StructureMapConfiguration
{
public static class BootStrapper
{
public static void SetupContainer()
{
ObjectFactory.Configure(s => s.For<ITwitterService>().Use<TwitterService>());
}
}
}

En una sola línea, le estoy indicando que para la interfaz ITwitterService, debemos usar una instancia de la clase TwitterService. La magia de todo esto es que, si el día de mañana esta interfaz es implementada por otra clase, y además esta interfaz es usada en numerosos sitios de nuestra aplicación, solamente debemos modificar esta línea para que la clase que la implementa comience a servirse como dependencia en los casos que lo requiera.

CONTROLADORES ASP.NET MVC YSTRUCTUREMAP

Por otro lado, si lo que queremos es trabajar con ASP.NET MVC, debemos realizar una serie de cambios: Cuando nosotros hacemos una petición en una aplicación con ASP.NET MVC, la clase ControllerBuilder genera de forma automática el controlador solicitado, se despacha la petición y el controlador finaliza. Para poder utilizar las propiedades de StructureMap, necesitamos crear una clase que herede de la factoría de controladores. De esta manera, controlaremos el momento en el cual se solicita una instancia de un controlador y, si este tiene dependencias, poder administrarlas con la configuración realizada anteriormente para StructureMap en la clase BootStraper.

using System;
using System.Web.Mvc;
using System.Web.Routing;
using StructureMap;

namespace IoC.StructureMapConfiguration
{
internal class StructureControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType != null)
return ObjectFactory.GetInstance(controllerType) as IController;

return base.GetControllerInstance(requestContext, controllerType);
}
}
}

INICIALIZANDO LA CONFIGURACIÓN DE STRUCTUREMAP Y ASOCIADO LA NUEVA FACTORÍA DE CONTROLADORES

Para finalizar, necesitamos inicializar tanto la configuración creada en BootStraper, para que StructureMap reconozca la interfaz especificada, como la asignación de la nueva factoría de controladores a la aplicación ASP.NET MVC. La mejor ubicación para este caso concreto, podría ser el archivo Global.asax.

using System.Web.Mvc;
using System.Web.Routing;
using IoC.StructureMapConfiguration;

namespace IoC
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801

public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);

}

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterRoutes(RouteTable.Routes);

BootStrapper.SetupContainer();

ControllerBuilder.Current.SetControllerFactory(new StructureControllerFactory());

}
}
}

Si arrancamos la aplicación, comprobaríamos que efectivamente se crea un controlador con su dependencia y delegamos esta acción gracias a la Inversión de Control.

Adjunto el proyecto por si fuera de utilidad.

¡Saludos!

Gracias a Hadi Hariri por sus enseñanzas 🙂

T4MVC: Eliminando literales de controladores, acciones y vistas.

T4MVC es una plantilla T4 (Text Template Transformation Toolkit), creada por David Ebbo, que genera de forma automática un conjunto de helpers que nos ayudarán a eliminar el uso de literales de nuestra aplicación ASP.NET MVC. Gracias al uso de esta plantilla, podemos utilizar intellisense para localizar el nombre de Controladores, acciones, vistas, scripts etcétera y conseguir un código con menos errores y mucho más mantenible.

Para poder descargarla, bastará con acceder al proyecto ubicado en Codeplex

INSTALACIÓN

La generación del código, se consigue nada más agregar los archivos T4MVC.tt  y T4MVC.settings.t4

¿Por qué es necesario agregar también el archivo .settings?

El archivo T4MVC.settings.t4 contiene un conjunto de constantes que ayudarán a generar el código en base a ellas. Por ejemplo:

// The prefix used for things like MVC.Dinners.Name and MVC.Dinners.Delete(Model.DinnerID)
const string HelpersPrefix = "MVC";

Esta constante nos indica el prefijo que precede a los helpers generados por T4MVC.

// The folder under the project that contains the areas
const string AreasFolder = "Areas";

El nombre de la carpeta que agrupa las áreas.

// The folder under the project that contains the controllers
const string ControllersFolder = "Controllers";

// The folder under the project that contains the views
const string ViewsRootFolder = "Views";

El nombre de las carpetas que contienen los controladores y las vistas.

// Folders containing static files for which links are generated (e.g. Links.Scripts.Map_js)
readonly string[] StaticFilesFolders = new string[] {
"Scripts",
"Content",
};

La carpeta donde figuran los archivos javascript y los estilos.

// If true,the template output will be split into multiple files.
static bool SplitIntoMultipleFiles = true;

La plantilla generará varios archivos de salida.

Si modificamos cualquiera de las constantes y compilamos la solución, se regenerará el código de salida de la plantilla.

En el momento de agregar los archivos, o al recompilar, nos aparecerá el siguiente cuadro de diálogo:

Solamente es un aviso y nos están advirtiendo de que el archivo que estamos intentando agregar/recompilar puede dañar nuestro equipo 🙂 y que no se ejecute si proviene de una fuente de la que no confiamos. En este momento podemos pulsar OK, lo cual significa que arrancará la plantilla de forma automática o bien podemos cancelar y se parará el proceso. Pulsamos OK y arriesgamos 😉

Si nos fijamos en el Solution explorer, ha generado varios archivos .cs ligados a la plantilla .tt.

Como la constante SplitIntoMultipleFiles es igual a true, nos generará tantos archivos como controladores tenga el proyecto.

EJEMPLOS

Controladores


Acciones / Vistas


Links dentro de Content/Scripts


En el controlador

return RedirectToAction("Index");

T4MVC

return RedirectToAction(MVC.Tweet.Actions.Index());  

Global.asax

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Tweet", action = "Index", id = "" } // Parameter defaults
);

T4MVC

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = MVC.Tweet.Name, action = MVC.Tweet.ActionNames.Index, id = "" } // Parameter defaults
);

Scripts

<script src="../../Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>

T4MVC

<script src="<%=Links.Scripts.jquery_1_3_2_min_js %>" type="text/javascript"></script>

Render Partial

<% Html.RenderPartial("TimeLine"); %>

T4MVC

<% Html.RenderPartial(MVC.Tweet.Views.TimeLine); %>

Action Links

<%=Html.ActionLink("Back to TimeLine", "Index") %>

T4MVC

<%=Html.ActionLink("Back to TimeLine", MVC.Tweet.Actions.Index()) %>


¡Saludos!