Ninject. Hola Mundo (I). Qué es la inyección de dependencias?
Tras ver en el anterior artículo lo que es la inyección de dependencias, en este artículo veremos que ventajas nos proporciona recurrir a un contenedor de dependencias como Ninject y cómo utilizarlo.
Al utilizar una interfaz en la clase Persona y su constructor puede paracer, a primera vista, que no hemos hecho gran cosa, a parte de cambiar la dependencia de sitio. Pero pensemos que, en lugar de tener una carpeta Model en la aplicación de consola, tuviéramos una librería de entidades con la lógica de negocio. Esta implementación nos permitiría cambiar la funcionalidad de la aplicación (emitir el mensaje "Hola a todos" en lugar de "Hola mundo") sin necesidad de realizar ningún cambio en la librería. De la misma forma, si queremos crear tests unitarios para la clase Persona, podremos realizarlos creando instancias de Persona que utilicen instancias de objetos "Mock" que implementen la interfaz IMensaje.
Podemos comprobar ambos casos.
Voy a añadir a la solución un nuevo proyecto de prueba NinjectHelloWorld.Tests.
Para crear el test sobre la clase Persona debemos agregarle al proyecto de test una referencia al proyecto NinjectHelloWorld.
En el proyecto de prueba he creado un archivo PersonaTest.cs/PersonaTest.vb con un método de prueba para el método Saludar de la clase Persona:
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using NinjectHelloWorld.Model; namespace NinjectHelloWorld.Tests { [TestClass] public class PersonaTest { private class MockMensaje: IMensaje { private bool _mensajeEmitido = false; public bool MensajeEmitido { get { return _mensajeEmitido; } } public void Emitir() { _mensajeEmitido = true; } } [TestMethod] public void TestSaludar() { MockMensaje mensaje = new MockMensaje(); Persona personaTest = new Persona(mensaje); personaTest.Saludar(); Assert.IsTrue(mensaje.MensajeEmitido); } } }
Imports System.Text Imports Microsoft.VisualStudio.TestTools.UnitTesting <TestClass()> Public Class PruebaTest Private Class MockMensaje Implements IMensaje Private _mensajeEmitido As Boolean = False Public ReadOnly Property MensajeEmtitdo() As Boolean Get Return _mensajeEmitido End Get End Property Public Sub Emitir() Implements IMensaje.Emitir _mensajeEmitido = True End Sub End Class <TestMethod()> Public Sub TestSaludar() Dim mensaje As New MockMensaje() Dim personaTest As New Persona(mensaje) personaTest.Saludar() Assert.IsTrue(mensaje.MensajeEmtitdo) End Sub End Class
He definido una clase MockMensaje que implementa la interfaz IMensaje para utilizarla en el test. La clase MockMensaje tiene una propiedad MensajeEmitido que indica si ha sido llamado el método Emitir. Al constructor de Persona le paso una instancia de MockMensaje de forma que puedo comprobar si el método Emitir ha sido llamado chequeando la propiedad MensajeEmitido.
Si ejecutamos el test (Menú Prueba - Ejecutar - Todas las pruebas) comprobaremos que pasa sin ningún problema.
De la misma forma si modificamos el método MandarSaludo para que utilice la clase SaludoTodos:
private static void MandarSaludo() { Persona persona = new Persona(new SaludoTodos()); persona.Saludar(); }
Private Sub MandarSaludo() Dim persona As New Persona(New SaludoTodos()) persona.Saludar() End Sub
Habremos modificado la funcionalidad de la aplicación sin necesidad de realizar modificaciones en el modelo.
Usando Ninject
Sin embargo, aunque hemos mejorado en lo que a nivel de dependencias entre los módulos de nuestra aplicación se refiere, para modificar el comportamiento de la aplicación usando la clase SaludoTodos nos hemos visto obligados a modificar el método MandarSaludo. Para evitar esto podemos agrupar todas las dependencias de la aplicación en un contenedor de dependencias como Ninject.
Lo primero que haremos es añadir al proyecto el paquete Nuget de Ninject.
Para configurar las dependencias de la aplicación con Ninject en primer lugar definiremos un objeto kernel, a través del cual definiremos las dependencias.
Una vez definidas las dependencias utilizaremos este mismo objeto kernel para crear las instancias de las diferentes clases.
using Ninject; using NinjectHelloWorld.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NinjectHelloWorld { class Program { private static IKernel kernel; static void Main(string[] args) { kernel = new StandardKernel(); kernel.Bind<IMensaje>().To<SaludoMundo>(); MandarSaludo(); Console.ReadKey(); } private static void MandarSaludo() { Persona persona = kernel.Get<Persona>(); persona.Saludar(); } } }
Imports Ninject Module Main Private kernel As IKernel Sub Main() kernel = New StandardKernel() kernel.Bind(Of IMensaje)().To(Of SaludoMundo)() MandarSaludo() Console.ReadKey() End Sub Private Sub MandarSaludo() Dim persona As Persona = kernel.Get(Of Persona)() persona.Saludar() End Sub End Module
De esta forma la interfaz IMensaje queda enlazada a la clase SaludoMundo y, cuando Ninject tiene que crear la instancia de una clase y detecta que en el constructor hay un parámetro de la interfaz IMensaje automáticamente genera una instancia de la clase SaludoMundo y se la pasa al constructor.
Aún podemos separar más la localización de la definición de dependencias creando un módulo de Ninject. El módulo es una clase que implementa la interfaz INinjectModule. Para simplificar, lo que haré es definir una clase que herede de NinjectModule y sobrescribir el método Load en el que configuraré las dependencias de la aplicación.
Para ello he creado una nueva carpeta Ninject en el proyecto y, en la carpeta, un archivo DependencyResolver.cs/DependencyResolver.vb
using Ninject.Modules; using NinjectHelloWorld.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NinjectHelloWorld.Ninject { public class DependencyResolver: NinjectModule { public override void Load() { Bind<IMensaje>().To<SaludoMundo>(); } } }
Imports Ninject.Modules Public Class DependencyResolver Inherits NinjectModule Public Overrides Sub Load() Bind(Of IMensaje)().To(Of SaludoMundo)() End Sub End Class
Ahora no hay más que indicar en el constructor del kernel el módulo de Ninject que debe utilizar para resolver las dependencias.
static void Main(string[] args) { kernel = new StandardKernel(new DependencyResolver()); MandarSaludo(); Console.ReadKey(); }
Sub Main() kernel = New StandardKernel(New DependencyResolver()) MandarSaludo() Console.ReadKey() End Sub
Como iremos viendo en los próximos artículos Ninject nos permite controlar la instanciación de los objetos de nuestra aplicación.
Si, por ejemplo, queremos utilizar una única instancia de la clase SaludoMundo a lo largo de toda la aplicación no tenemos más que indicarlo al contenedor de dependencias:
public class DependencyResolver: NinjectModule { public override void Load() { Bind<IMensaje>().To<SaludoMundo>().InSingletonScope(); } }
Public Class DependencyResolver Inherits NinjectModule Public Overrides Sub Load() Bind(Of IMensaje)().To(Of SaludoMundo)().InSingletonScope() End Sub End Class
De esta forma Ninject creará una instancia de la clase SaludoMundo cuando se solicite por primera vez un objeto del tipo IMensaje y devolverá esa misma instancia cada vez que se solicite un nuevo objeto IMensaje a lo largo de toda la aplicación. Y todo ello cambiando una única instrucción en la clase responsable de resolver las dependencias.
Sería de gran ayuda seguir con más ejemplos cada vez más complejos acerca de Ninject y su uso sobre todo en ASP .NET MVC .
ResponderEliminar