martes, 16 de junio de 2015

Ninject. Hola Mundo (y II). Sacándole partido al contenedor de dependencias

Artículo anterior:
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.

Proyecto de prueba unitaria NinjectHelloWorld.Tests

Para crear el test sobre la clase Persona debemos agregarle al proyecto de test una referencia al proyecto NinjectHelloWorld.

Añadir referencia a 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.

Test Saludar Ok

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.

Mensaje "Hola Todos"



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.

Instalar 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.

No hay comentarios:

Publicar un comentario