viernes, 12 de junio de 2015

Ninject. Hola Mundo (I). ¿Qué es la inyección de dependencias?

Con este artículo pretendo iniciar un pequeño tutorial basado en ejemplos para mostrar cómo utilizar un framework de inyección de dependencias como Ninject.

En primer lugar crearé un primer ejemplo en el que trataré de mostrar qué es eso de la inyección de dependencias, porqué nos puede resultar útil y cómo Ninject puede ayudarnos.



Qué es eso de la inyección de dependencias

Básicamente lo que pretendemos conseguir con la inyección de dependencias es, precisamente, eliminar dependencias. El objetivo es desarrollar las diferentes funcionalidades de nuestra aplicación (clases, métodos, etc) de forma que sean módulos independientes entre sí, precisamente delegaremos en un "contenedor de dependencias" como Ninject la responsabilidad de "inyectar" las dependencias entre dichos módulos (de ahí el nombre). Eliminar esas dependencias permitirá que nuestro código sea mucho más mantenible, extensible y testeable.

No se ha entendido nada, ¿verdad? Si al final del artículo vuelves a leer este párrafo todo tendrá mucho más sentido.

Una imagen vale más que mil palabras y un ejemplo más que muchas definiciones, así que, en lugar de enfrascarme con engorrosas definiciones, voy a mostrar un ejemplo en el que veremos cual es el problema que se pretende solucionar y qué tipo de solución propone la inyección de dependencias.

Hola Mundo

Desde el Visual Studio he creado un nuevo proyecto de consola NinjectHelloWorld dentro de una solución llamada Ninject.

Crear proyecto HelloWorld

Como en la mayoría de las aplicaciones "Hola Mundo" el objetivo será mostrar en pantalla precisamente este mensaje.

En el proyecto voy a crear una carpeta Model y dentro de ésta dos archivos Persona.cs/Persona.vb y Mensajes.cs/Mensajes.vb. En el primero crearé la clase Persona que será la responsable de emitir el saludo, mientras que en el archivo Mensajes.cs/Mensajes.vb crearé las clases responsables de formatear el mensaje, por ahora una única clase SaludoMundo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NinjectHelloWorld.Model
{

    public class SaludoMundo
    {
        public void Emitir()
        {
            Console.WriteLine("Hola Mundo");
        }
    }

}
Public Class SaludoMundo

    Public Sub Emitir()
        Console.WriteLine("Hola Mundo")
    End Sub

End Class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NinjectHelloWorld.Model
{
    public class Persona
    {
        private SaludoMundo _saludo;

        public Persona()
        {
            _saludo = new SaludoMundo();
        }

        public void Saludar()
        {
            _saludo.Emitir();
        }
    }
}
Public Class Persona

    Dim _saludo As SaludoMundo

    Public Sub New()
        _saludo = New SaludoMundo()
    End Sub

    Public Sub Saludar()
        _saludo.Emitir()
    End Sub

End Class

La clase SaludoMundo expone un método Emitir que escribe el mensaje en la salida de la consola, mientras que la clase Persona crea una instancia de SaludoMundo y expone un método Saludar que llama al método Emitir del saludo.
Para que nuestra aplicación muestre el mensaje deseado simplemente crearé un método MandarSaludo en el archivo Program.cs/Main.vb que cree una instancia de Persona e invoque el método Saludar.

using NinjectHelloWorld.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NinjectHelloWorld
{


    class Program
    {
        static void Main(string[] args)
        {
            MandarSaludo();
            Console.ReadKey();
        }

        private static void MandarSaludo()
        {
            Persona persona = new Persona();
            persona.Saludar();
        }
    }
}
Module Main

    Sub Main()
        MandarSaludo()
        Console.ReadKey()
    End Sub

    Private Sub MandarSaludo()
        Dim persona As New Persona()
        persona.Saludar()
    End Sub

End Module

Si arrancamos la aplicación obtendremos el resultado deseado.

Mensaje Hola Mundo



Sin embargo si nos fijamos en la clase Persona podremos comprobar que tiene una fuerte dependencia de la clase SaludoMundo. Si nos creásemos, por ejemplo, una nueva clase SaludoTodos en el archivo Mensajes.cs/Mensajes.vb:

    public class SaludoTodos
    {
        public void Emitir()
        {
            Console.WriteLine("Hola Todos");
        }
    }
Public Class SaludoTodos

    Public Sub Emitir()
        Console.WriteLine("Hola Todos")
    End Sub

End Class

y pretendiéramos que este fuese el mensaje a mostrar, deberíamos modificar de forma importante la clase Persona. O si, por ejemplo, creásemos un test unitario para el método Saludar de la clase Persona nada podría garantizarnos, en caso de fallar, que el error se encuentre en la clase Persona y no en la clase SaludoMundo. En este último caso la solución ideal sería el poder crearnos un objeto "Mock" que reemplace al objeto de la clase SaludoMundo a la hora de probar el método Saludar. Es decir, en ambos casos necesitaríamos que la clase Persona nos permitiese utilizar un objeto que no sea necesariamente de la clase SaludoMundo.

La solución es obvia: utilizar interfaces.

Voy a implementar la interfaz IMensaje que expondrá un único método Emitir y que implementarán tanto la clase SaludoMundo como SaludoTodos. En la clase Persona reemplazaré también la referencia a la clase SaludoMundo por la nueva interfaz.

    public interface IMensaje
    {
        void Emitir();
    }

    public class SaludoMundo: IMensaje
    {
        public void Emitir()
        {
            Console.WriteLine("Hola Mundo");
        }
    }

    public class SaludoTodos: IMensaje
    {
        public void Emitir()
        {
            Console.WriteLine("Hola Todos");
        }
    }
Public Interface IMensaje
    Sub Emitir()
End Interface

Public Class SaludoMundo
    Implements IMensaje

    Public Sub Emitir() Implements IMensaje.Emitir
        Console.WriteLine("Hola Mundo")
    End Sub

End Class

Public Class SaludoTodos
    Implements IMensaje

    Public Sub Emitir() Implements IMensaje.Emitir
        Console.WriteLine("Hola Todos")
    End Sub

End Class

Sin embargo seguimos teniendo una referencia a la clase SaludoMundo en la clase Persona: la llamada al constructor. No podemos crear una instancia de una interfaz por lo que tendremos que buscar alguna alternativa. Lo que voy a hacer es modificar el constructor de la clase Persona para que reciba como parámetro la instancia de un objeto que implemente la interfaz IMensaje. Para ello tendré que cambiar también la llamada al constructor de la clase Persona en el método MandarSaludo.

    public class Persona
    {
        private IMensaje _saludo;

        public Persona(IMensaje saludo)
        {
            _saludo = saludo;
        }

        public void Saludar()
        {
            _saludo.Emitir();
        }
    }
Public Class Persona

    Dim _saludo As IMensaje

    Public Sub New(saludo As IMensaje)
        _saludo = saludo
    End Sub

    Public Sub Saludar()
        _saludo.Emitir()
    End Sub

End Class


 private static void MandarSaludo()
 {
  Persona persona = new Persona(new SaludoMundo());
  persona.Saludar();
 }
    Private Sub MandarSaludo()
        Dim persona As New Persona(New SaludoMundo())
        persona.Saludar()
    End Sub

En esto consiste la inyección de dependencias, más concretamente inyección de dependencias "por constructor", que es uno de los patrones más utilizados. Si, en lugar de modificar el constructor, hubiésemos creado una propiedad pública a través de la cual le asignáramos la instancia de IMensaje, estaríamos hablando de otro patrón diferente conocido como inyección de dependencias "por propiedad".

Artículo siguiente:
Ninject. Hola Mundo (y II). Sacándole partido al contenedor de dependencias

1 comentario:

  1. Excelente artículo, había visto ejercicios en donde utilizaban el ninject pero nunca había entendido para qué, en sí hasta llegué a pensar que se estaban complicando la vida. Con esto que ilustras me queda totalmente clara su utilidad

    ResponderEliminar