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:
- 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.
- 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.
- 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.
- Complicamos la reutilización de la clase, puesto que siempre va a ir unida a los componentes de los que depende.
- 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.
- 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:
- La implementación de
InvoiceServices
queda totalmente desacoplada, puesto que no depende de ningún componente específico para funcionar, sólo de contratos.
- 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.
- 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.
- La clase será mucho más reutilizable porque no depende de otros componentes, sino de abstracciones.
- 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: