Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=9f4abc5e-cb0b-4fb3-b905-aabbff3a0339

Qué es la Inyección de Dependencias y cómo funciona

La inversión de dependencias es un principio que describe un conjunto de técnicas destinadas a disminuir el acoplamiento entre los componentes de una aplicación. Es uno de los principios SOLID más populares y utilizados en la creación de aplicaciones, frameworks y componentes por las ventajas que aporta a las mismas.

La inversión de dependencias suele también conocerse como inversión de control. En inglés, los términos más frecuentemente utilizados son "dependency inversion", abreviado como "DI", e "inversion of control" o simplemente "IoC".

Muy resumidamente, el Principio de Inversión de Dependencias propone evitar las dependencias rígidas entre componentes mediante las siguientes técnicas:

  • Utilizar abstracciones (interfaces) en lugar de referencias directas entre clases, lo que facilita que podamos reemplazar componentes con suma facilidad.
  • Hacer que una clase reciba referencias a los componentes que necesite para funcionar, en lugar de permitir que sea ella misma quien los instancie de forma directa o a través de factorías.

La inyección de dependencias es una de las técnicas utilizadas para implementar el principio de inversión de dependencias.

Seguro que entiendes mejor estos conceptos si vemos algo de código. Observa el siguiente ejemplo, escrito en C# (pero valdría para cualquier lenguaje) aún sin usar inyección de dependencias, donde se muestra una clase llamada InvoiceServices cuyo funcionamiento depende, como mínimo, de otras dos clases externas, InvoiceRepository e EmailNotifier:

public class InvoiceServices
{
    ...
    public void Remove(int invoiceId)
    {
        using (var invoiceRepository = new InvoiceRepository())
        {
            var removed = invoiceRepository.Remove(invoiceId);
            if (removed)
            {
                var notifier = new EmailNotifier();
                notifier.NotifyAdmin($"Invoice {invoiceId} removed");
            }
        }
    }
}

El código del método Remove(), aunque aparentemente correcto, presenta algunos problemas:

  1. Tiene bastantes líneas de código de "fontanería", dedicadas a instanciar y preparar las dependencias, en lugar de centrarse en su misión, que es eliminar una factura y notificar al administrador.
  2. Observa además que, muchas de esas líneas deberían repetirse en otros métodos de la clase que requirieran los servicios de estos componentes. Por ejemplo, otros métodos, como Add() o Update(), probablemente necesitarían acceder al repositorio de facturas y quizás también al componente de notificación.
  3. Estamos atando inexorablemente la implementación de InvoiceServices a InvoiceRepository e EmailNotifier. Cualquier modificación en estas últimas podría afectar a la primera, o incluso requerir cambios en ésta.
  4. Complicamos la reutilización de la clase, puesto que siempre va a ir unida a los componentes de los que depende.
  5. No quedan claras las dependencias de la clase. Para conocerlas deberíamos leer todo su código y ver qué clases externas utiliza. Aunque en ese ejemplo no es un problema porque es poco código, en clases más extensas sí sería bastante complicado determinarlas.
  6. Dificultamos la realización de pruebas unitarias, puesto que no hay forma de probar únicamente el correcto funcionamiento del método Remove() de InvoiceServices sin probar al mismo tiempo el funcionamiento de las clases de las que depende.

Utilizando el principio de Inyección de Dependencias, el código anterior podríamos transformarlo en el siguiente:

public class InvoiceServices: IInvoiceServices
{
    private readonly IInvoiceRepository _invoiceRepository;
    private readonly INotifier _notifier;

    public InvoiceServices (IInvoiceRepository invoiceRepository, INotifier notifier)
    {
        _invoiceRepository = invoiceRepository;
        _notifier = notifier;
    }
    ...
    public void Remove(int invoiceId)
    {
        var removed = _invoiceRepository.Remove(invoiceId);
        if (removed)
        {
            _notifier.NotifyAdmin($"Invoice {invoiceId} removed");
        }
    }
}

Observa que, ahora, las dependencias de la clase las recibimos en el constructor y las almacenamos localmente en miembros de instancia.

En la práctica podemos utilizar un sistema de inyección de dependencias. Un sistema de inyección de dependencias es el encargado de instanciar las clases que necesitemos y suministrarnos ("inyectar") las dependencias enviando los parámetros oportunos al constructor.

Existe otra forma de indicar las dependencias de una clase que, en lugar de utilizar el constructor para recibir las dependencias, utiliza propiedades decoradas con algún tipo de atributo que el inyector de dependencias es capaz de reconocer. Por ejemplo, en el siguiente fragmento se utiliza el atributo [Dependency] para indicar al contenedor que el valor de dichas propiedades debe ser inyectado. El resultado sería totalmente equivalente a usar inyección en el constructor:

public class InvoiceServices: IInvoiceServices
{
    [Dependency]
    private readonly IInvoiceRepository _invoiceRepository;
    [Dependency]
    private readonly INotifier _notifier;
    ...
    // Otros miembros de la clase
}

¡Ojo!: el nombre del atributo [Dependency] es solo a nivel ilustrativo, no existe en .NET. Ahora mismo estamos hablando de manera genérica sobre estas técnicas.

En cualquiera de las dos opciones, fíjate en que nos abstraemos de implementaciones concretas mediante el uso de interfaces. No nos importarán los tipos concretos que lleguen al constructor como dependencias, siempre que cumplan los contratos definidos por sus interfaces. Por ejemplo, para la interfaz INotifier podría llegarnos la clase EmailNotifier que usábamos en el ejemplo anterior, o bien una instancia de TwitterNotifier o MobilePushNotifier; nos da igual, lo importante es que dispongan del método NotifyAdmin(), que es lo que en realidad usamos de ellas.

De esta forma conseguiremos las siguientes ventajas:

  1. La implementación de InvoiceServices queda totalmente desacoplada, puesto que no depende de ningún componente específico para funcionar, sólo de contratos.
  2. Para conocer las dependencias de la clase basta con echar un vistazo a su constructor o a las propiedades decoradas con el atributo usado por el marco de trabajo para marcar los miembros inyectables, por lo que simplificamos su lectura y facilitamos su comprensión.
  3. Los métodos pueden centrarse ahora en lograr su cometido porque las dependencias ya están disponibles a nivel de instancia. Esto nos lleva a disponer de un código más conciso, limpio, fácil de escribir y de leer. Observa que puedes entender el método Remove() del segundo ejemplo de un rápido vistazo, mientras que en el primer ejemplo necesitabas una lectura algo más detenida.
  4. La clase será mucho más reutilizable porque no depende de otros componentes, sino de abstracciones.
  5. Podemos realizar fácilmente pruebas unitarias de esta clase de forma aislada, enviándole dependencias falsas o controladas (fakes, stubs, mocks...) desde los métodos de test.

Para que todo esto funcione necesitamos un componente, habitualmente denominado contenedor de inversión de control (IoC Container en inglés) o simplemente contenedor de inyección de dependencias, cuya única responsabilidad es crear clases con todas sus dependencias asociadas. A todos los efectos, actúa como una factoría a la que podemos solicitar objetos de tipos específicos que él, internamente, se encargará de instanciar y gestionar.

Para ello, el contenedor dispone de un registro de servicios y equivalencias entre abstracciones y tipos concretos que usa para resolver las dependencias cuando es necesario. Por ejemplo, para el escenario anterior, el contenedor de IoC podría disponer de la siguiente información:

Abstracción Clase concreta
IInvoiceServices InvoiceServices
IInvoiceRepository InvoiceRepository
INotifier EmailNotifier

De esta forma, cuando una aplicación requiere una instancia de InvoiceServices, lo que haría, en lugar de crearla directamente, es solicitar al contenedor IoC un objeto IInvoiceServices. Éste sabría que la clase concreta a crear es InvoiceServices y analizaría los parámetros de su constructor o propiedades decoradas con un atributo apropiado según el inyector que usemos, detectando que a su vez depende de dos abstracciones: IInvoiceRepository e INotifier. Así, atendiendo a su registro interno, primero crearía objetos de tipo InvoiceRepository e EmailNotifier y luego crearía la instancia de InvoiceServices suministrándole como dependencias las instancias anteriores.

Por supuesto, si InvoiceRepository o EmailNotifier necesitaran a su vez satisfacer otras dependencias para poder ser instanciadas, se haría exactamente lo mismo, resolviendo todas las dependencias de forma recursiva hasta conseguir crear los objetos de los tipos solicitados.

 

Fecha de publicación:
José María Aguilar José María atesora una amplísima experiencia trabajando en el mundo del desarrollo de software (programador, analista, responsable de informática, consultor, director técnico), principalmente con tecnologías Microsoft. Actualmente trabaja como consultor y desarrollador independiente, ofreciendo servicios tecnológicos a empresas e instituciones.
Es un reconocido experto en desarrollo web en todo el mundo, y es autor del libro de Microsoft Press "SignalR Programming in Microsoft ASP.NET".
Escribe regularmente artículos sobre ASP.NET MVC y otros temas relacionados con el desarrollo de software en su blog.
Puedes seguirlo en Twitter en @jmaguilar. Ver todos los posts de José María Aguilar
Archivado en: General

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Comentarios (5) -

Iván Trujillo
Iván Trujillo

Excelentemente explicado, muchas gracias.

Responder

Muy buena explicación. Gracias

Responder

Juan Sarria
Juan Sarria

Excelente. Muy claro. Gracias.

Responder

Una explicación deliciosa! Gracias

Responder

Ramses de la Rosa
Ramses de la Rosa

Muy clara la explicación. Mil gracias!

Responder

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.