¿Por qué se usa la inyección de dependencia?


536

Estoy tratando de entender las inyecciones de dependencia (DI), y una vez más fallé. Simplemente parece tonto. Mi código nunca es un desastre; Apenas escribo funciones e interfaces virtuales (aunque lo hago una vez en una luna azul) y toda mi configuración se serializa mágicamente en una clase usando json.net (a veces usando un serializador XML).

No entiendo qué problema resuelve. Parece una forma de decir: "hola. Cuando te encuentres con esta función, devuelve un objeto que sea de este tipo y use estos parámetros / datos".
Pero ... ¿por qué usaría eso? Tenga en cuenta que nunca he necesitado usarlo objecttambién, pero entiendo para qué sirve.

¿Cuáles son algunas situaciones reales en la creación de un sitio web o una aplicación de escritorio donde uno usaría DI? Puedo encontrar casos fácilmente de por qué alguien quiere usar interfaces / funciones virtuales en un juego, pero es extremadamente raro (lo suficientemente raro como para no recordar una sola instancia) usar eso en un código que no sea del juego.


3
Esto también puede ser información útil: martinfowler.com/articles/injection.html
ta.speot.is




55
Otra explicación muy simple de DI: codearsenal.net/2015/03/…
ybonda

Respuestas:


840

Primero, quiero explicar una suposición que hago para esta respuesta. No siempre es cierto, pero con bastante frecuencia:

Las interfaces son adjetivos; las clases son sustantivos

(En realidad, también hay interfaces que son sustantivos, pero quiero generalizar aquí).

Entonces, por ejemplo, una interfaz puede ser algo como IDisposable, IEnumerableo IPrintable. Una clase es una implementación real de una o más de estas interfaces: Listo Mapambas pueden ser implementaciones de IEnumerable.

Para entender el punto: a menudo sus clases dependen unas de otras. Por ejemplo, podría tener una Databaseclase que acceda a su base de datos (¡ja, sorpresa! ;-)), pero también desea que esta clase inicie sesión para acceder a la base de datos. Supongamos que tiene otra clase Logger, luego Databasetiene una dependencia para Logger.

Hasta aquí todo bien.

Puede modelar esta dependencia dentro de su Databaseclase con la siguiente línea:

var logger = new Logger();

Y todo está bien. Está bien hasta el día en que te das cuenta de que necesitas un montón de registradores: a veces quieres iniciar sesión en la consola, a veces en el sistema de archivos, a veces usando TCP / IP y un servidor de registro remoto, y así sucesivamente ...

Y, por supuesto, NO desea cambiar todo su código (mientras tanto, tiene miles de millones) y reemplazar todas las líneas

var logger = new Logger();

por:

var logger = new TcpLogger();

Primero, esto no es divertido. En segundo lugar, esto es propenso a errores. Tercero, este es un trabajo estúpido y repetitivo para un mono entrenado. Entonces, ¿Qué haces?

Obviamente, es una buena idea introducir una interfaz ICanLog(o similar) implementada por todos los distintos registradores. Entonces, el paso 1 en su código es que usted hace:

ICanLog logger = new Logger();

Ahora la inferencia de tipo ya no cambia de tipo, siempre tiene una única interfaz para desarrollar. El siguiente paso es que no desea tener new Logger()una y otra vez. Así que pones la confiabilidad para crear nuevas instancias en una sola clase de fábrica central, y obtienes código como:

ICanLog logger = LoggerFactory.Create();

La fábrica misma decide qué tipo de registrador crear. A su código ya no le importa, y si desea cambiar el tipo de registrador que está utilizando, cámbielo una vez : dentro de la fábrica.

Ahora, por supuesto, puede generalizar esta fábrica y hacer que funcione para cualquier tipo:

ICanLog logger = TypeFactory.Create<ICanLog>();

En algún lugar, este TypeFactory necesita datos de configuración que clase real para instanciar cuando se solicita un tipo de interfaz específico, por lo que necesita una asignación. Por supuesto, puede hacer esta asignación dentro de su código, pero luego un cambio de tipo significa volver a compilar. Pero también podría poner esta asignación dentro de un archivo XML, por ejemplo. Esto le permite cambiar la clase realmente utilizada incluso después del tiempo de compilación (!), Lo que significa dinámicamente, ¡sin recompilar!

Para darle un ejemplo útil de esto: piense en un software que no se registra normalmente, pero cuando su cliente llama y solicita ayuda porque tiene un problema, todo lo que le envía es un archivo de configuración XML actualizado, y ahora tiene registro habilitado, y su soporte puede usar los archivos de registro para ayudar a su cliente.

Y ahora, cuando reemplaza un poco los nombres, termina con una implementación simple de un Localizador de servicios , que es uno de los dos patrones para la Inversión de control (ya que invierte el control sobre quién decide qué clase exacta instanciar).

En general, esto reduce las dependencias en su código, pero ahora todo su código depende del localizador de servicio único central.

La inyección de dependencia es ahora el siguiente paso en esta línea: simplemente elimine esta dependencia única del localizador de servicios: en lugar de que varias clases soliciten al localizador de servicios una implementación para una interfaz específica, usted, una vez más, revierte el control sobre quién instancia qué .

Con la inyección de dependencia, su Databaseclase ahora tiene un constructor que requiere un parámetro de tipo ICanLog:

public Database(ICanLog logger) { ... }

Ahora su base de datos siempre tiene un registrador para usar, pero ya no sabe de dónde proviene este registrador.

Y aquí es donde entra en juego un marco DI: configura sus asignaciones una vez más y luego le pide a su marco DI que cree una instancia de su aplicación por usted. Como la Applicationclase requiere una ICanPersistDataimplementación, Databasese inyecta una instancia de , pero para eso primero debe crear una instancia del tipo de registrador configurado para ICanLog. Y así ...

Entonces, para resumir: la inyección de dependencias es una de las dos formas de eliminar dependencias en su código. Es muy útil para los cambios de configuración después del tiempo de compilación, y es una gran cosa para las pruebas unitarias (ya que hace que sea muy fácil inyectar stubs y / o simulacros).

En la práctica, hay cosas que no puede hacer sin un localizador de servicios (por ejemplo, si no sabe de antemano cuántas instancias necesita de una interfaz específica: un marco DI siempre inyecta solo una instancia por parámetro, pero puede llamar un localizador de servicios dentro de un bucle, por supuesto), por lo tanto, con mayor frecuencia cada marco DI también proporciona un localizador de servicios.

Pero básicamente, eso es todo.

PD: Lo que describí aquí es una técnica llamada inyección de constructor , también hay inyección de propiedades donde no hay parámetros de constructor, sino propiedades que se utilizan para definir y resolver dependencias. Piense en la inyección de propiedades como una dependencia opcional, y en la inyección del constructor como dependencias obligatorias. Pero la discusión sobre esto está más allá del alcance de esta pregunta.


77
Por supuesto, también puede hacerlo de esta manera, pero luego tiene que implementar esta lógica en cada clase que proporcionará soporte para la intercambiabilidad de la implementación. Esto significa un montón de código duplicado y redundante, y también significa que debe tocar una clase existente y volver a escribirla parcialmente, una vez que decida que la necesita ahora. DI le permite usar esto en cualquier clase arbitraria, no tiene que escribirlos de una manera especial (excepto definir dependencias como parámetros en el constructor).
Golo Roden

138
Esto es lo que nunca entiendo sobre DI: hace que la arquitectura sea mucho más complicada. Y sin embargo, como lo veo, el uso es bastante limitado. Los ejemplos son siempre los mismos: registradores intercambiables, modelo intercambiable / acceso a datos. A veces vista intercambiable. Pero eso es todo. ¿Estos pocos casos realmente justifican una arquitectura de software mucho más compleja? - Revelación completa: ya he usado DI con gran efecto, pero eso fue para una arquitectura de plug-in muy especial que no generalizaría.
Konrad Rudolph el

17
@GoloRoden, ¿por qué llama a la interfaz ICanLog en lugar de ILogger? ¿Trabajé con otro programador que a menudo hacía esto y nunca pude entender la convención? Para mí es como llamar a IEnumerable ICanEnumerate?
DermFrench

28
Lo llamé ICanLog, porque trabajamos muy a menudo con palabras (sustantivos) que no significan nada. Por ejemplo, ¿qué es un corredor? ¿Un manager? Incluso un repositorio no se define de una manera única. Y tener todas estas cosas como sustantivos es una enfermedad típica de los idiomas OO (ver steve-yegge.blogspot.de/2006/03/… ). Lo que quiero expresar es que tengo un componente que puede hacer el registro por mí, así que ¿por qué no llamarlo así? Por supuesto, esto también está jugando con el I como la primera persona, de ahí ICanLog (ForYou).
Golo Roden

18
La prueba de la unidad @David funciona bien: después de todo, una unidad es independiente de otras cosas (de lo contrario, no es una unidad). Lo que no funciona sin contenedores DI es una prueba simulada. Justo, no estoy convencido de que el beneficio de burlarse supere la complejidad añadida de agregar contenedores DI en todos los casos. Hago pruebas unitarias rigurosas. Raramente hago burlas.
Konrad Rudolph el

499

Creo que muchas veces las personas se confunden acerca de la diferencia entre la inyección de dependencia y un marco de inyección de dependencia (o un contenedor como se lo llama a menudo).

La inyección de dependencia es un concepto muy simple. En lugar de este código:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

escribes código como este:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

Y eso es. Seriamente. Esto te da muchas ventajas. Dos importantes son la capacidad de controlar la funcionalidad desde un lugar central (la Main()función) en lugar de distribuirla por todo el programa, y ​​la capacidad de probar más fácilmente cada clase de forma aislada (porque en su lugar puede pasar simulacros u otros objetos falsos a su constructor) de un valor real).

El inconveniente, por supuesto, es que ahora tiene una megafunción que conoce todas las clases utilizadas por su programa. Eso es con lo que los marcos DI pueden ayudar. Pero si tiene problemas para comprender por qué este enfoque es valioso, le recomiendo comenzar primero con la inyección de dependencia manual, para que pueda apreciar mejor lo que los diversos marcos pueden hacer por usted.


77
¿Por qué preferiría el segundo código en lugar del primero? el primero solo tiene la nueva palabra clave, ¿cómo ayuda esto?
user962206

17
@ user962206 piense cómo probaría A independientemente de B
jk.

66
@ user962206, también, piense en lo que sucedería si B necesitara algunos parámetros en su constructor: para instanciarlo, A tendría que conocer esos parámetros, algo que podría no estar relacionado con A (solo quiere depender de B , no de lo que B depende). Pasar una B ya construida (o cualquier subclase o simulacro de B para el caso) al constructor de A resuelve eso y hace que A dependa solo de B :)
epidemia

17
@ acidzombie24: Al igual que muchos patrones de diseño, DI no es realmente útil a menos que su base de código sea lo suficientemente grande como para que el enfoque simple se convierta en un problema. Mi intuición es que DI no será realmente una mejora hasta que su aplicación tenga más de aproximadamente 20,000 líneas de código y / o más de 20 dependencias de otras bibliotecas o marcos. Si su aplicación es más pequeña que eso, aún puede preferir programar en un estilo DI, pero la diferencia no será tan dramática.
Daniel Pryden

2
@DanielPryden, no creo que el tamaño del código importe tanto como lo dinámico que sea su código. Si agrega regularmente nuevos módulos que se ajustan a la misma interfaz, no tendrá que cambiar el código dependiente con tanta frecuencia.
FistOfFury

35

Como indicaron las otras respuestas, la inyección de dependencias es una forma de crear sus dependencias fuera de la clase que lo usa. Los inyecta desde el exterior y toma el control sobre su creación lejos del interior de su clase. Esta es también la razón por la cual la inyección de dependencia es una realización del principio de Inversión de control (IoC).

IoC es el principio, donde DI es el patrón. La razón por la que podría "necesitar más de un registrador" nunca se cumple, en lo que respecta a mi experiencia, pero la razón es que realmente lo necesita, cada vez que prueba algo. Un ejemplo:

Mi característica:

Cuando miro una oferta, quiero marcar que la vi automáticamente, para que no me olvide de hacerlo.

Puede probar esto así:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

Entonces, en algún lugar del OfferWeasel, crea un Objeto de oferta como este:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

El problema aquí es que es muy probable que esta prueba siempre falle, ya que la fecha que se establece será diferente de la fecha que se afirma, incluso si solo ingresa DateTime.Nowel código de prueba, podría estar apagado por un par de milisegundos y, por lo tanto, siempre fallan Una mejor solución ahora sería crear una interfaz para esto, que le permita controlar a qué hora se establecerá:

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

La interfaz es la abstracción. Uno es lo REAL, y el otro le permite fingir algún tiempo donde sea necesario. La prueba se puede cambiar así:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

Así, aplicaste el principio de "inversión de control", inyectando una dependencia (obteniendo la hora actual). La razón principal para hacer esto es para una prueba de unidad aislada más fácil, hay otras formas de hacerlo. Por ejemplo, una interfaz y una clase aquí son innecesarias ya que en C # las funciones se pueden pasar como variables, por lo que en lugar de una interfaz, puede usar a Func<DateTime>para lograr lo mismo. O, si adopta un enfoque dinámico, simplemente pasa cualquier objeto que tenga el método equivalente ( escritura de pato ), y no necesita una interfaz en absoluto.

Casi nunca necesitará más de un registrador. No obstante, la inyección de dependencia es esencial para el código tipado estáticamente como Java o C #.

Y ... También debe tenerse en cuenta que un objeto solo puede cumplir adecuadamente su propósito en tiempo de ejecución, si todas sus dependencias están disponibles, por lo que no tiene mucho uso configurar la inyección de propiedades. En mi opinión, todas las dependencias deberían satisfacerse cuando se llama al constructor, por lo que la inyección de constructor es lo que debe elegir.

Espero que haya ayudado.


44
Eso en realidad parece una solución terrible. Definitivamente escribiría un código más como sugiere la respuesta de Daniel Pryden, pero para esa prueba de unidad específica, simplemente haría DateTime. ¿Ahora antes y después de la función y verifico si el tiempo está en el medio? Agregar más interfaces / muchas más líneas de código me parece una mala idea.

3
No me gustan los ejemplos genéricos A (B) y nunca sentí que se requiere un registrador para tener 100 implementaciones. Este es un ejemplo que encontré recientemente y es una de las 5 formas de resolverlo, donde uno realmente incluyó el uso de PostSharp. Ilustra un enfoque clásico de inyección de ctor basado en clases. ¿Podría proporcionar un mejor ejemplo del mundo real de dónde encontró un buen uso para la DI?
evaluador

2
Nunca vi un buen uso para DI. Por eso escribí la pregunta.

2
No lo he encontrado útil. Mi código siempre es fácil de hacer. Parece que DI es bueno para grandes bases de código con código incorrecto.

1
Tenga en cuenta que incluso en pequeños programas funcionales, cada vez que tiene f (x), g (f) ya está utilizando la inyección de dependencia, por lo que cada continuación en JS contaría como una inyección de dependencia. Supongo que ya lo estás usando;)
evaluador

15

Creo que la respuesta clásica es crear una aplicación más desacoplada, que no tiene conocimiento de qué implementación se utilizará durante el tiempo de ejecución.

Por ejemplo, somos un proveedor de pagos central, trabajando con muchos proveedores de pagos en todo el mundo. Sin embargo, cuando se realiza una solicitud, no tengo idea de a qué procesador de pagos voy a llamar. Podría programar una clase con un montón de casos de interruptores, como:

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

Ahora imagine que ahora necesitará mantener todo este código en una sola clase porque no está desacoplado correctamente, puede imaginar que por cada nuevo procesador que admitirá, necesitará crear un nuevo caso de // switch para En cada método, esto solo se vuelve más complicado, sin embargo, al usar la Inyección de dependencia (o Inversión de control, como a veces se le llama, lo que significa que quien controla el funcionamiento del programa se conoce solo en tiempo de ejecución, y no complicación), puede lograr algo Muy ordenado y mantenible.

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** El código no se compilará, lo sé :)


+1 porque parece que estás diciendo que lo necesitas donde usas métodos / interfaces virtuales. Pero eso sigue siendo raro. Todavía me lo paso en como un new ThatProcessor()lugar de utilizar un marco

@ItaiS Puede evitar los innumerables interruptores con el patrón de diseño de fábrica de clase. Use la reflexión System.Reflection.Assembly.GetExecutingAssembly (). CreateInstance ()
domingo

@domenicr por supuesto! pero quería explicarlo en un ejemplo simplificado
Itai Sagi el

Estoy de acuerdo con la explicación anterior, excepto la necesidad de clase de fábrica. En el momento en que implementamos la clase de fábrica, es simplemente una cruda selección. La mejor explicación de lo anterior que encontré en el Capítulo de Poimorfismo y función virtual de Bruce Erkel. El DI verdadero debe estar libre de la selección y el tipo de objeto debe decidirse en tiempo de ejecución a través de la interfaz automáticamente. Ese es también el verdadero comportamiento polimórfico.
Arvind Krmar

Por ejemplo (según c ++) tenemos una interfaz común que toma solo la referencia a la clase base e implementa el comportamiento de su clase derivada sin selección. melodía vacía (Instrumento & i) {i.play (middleC); } int main () {Flauta de viento; melodía (flauta); } El instrumento es la clase base, el viento se deriva de él. Según c ++, la función virtual hace posible implementar el comportamiento de la clase derivada a través de una interfaz común.
Arvind Krmar

6

La razón principal para usar DI es que desea poner la responsabilidad del conocimiento de la implementación donde está el conocimiento. La idea de DI está muy en línea con la encapsulación y el diseño por interfaz. Si el front-end solicita algunos datos al back-end, entonces no es importante para el front-end cómo el back-end resuelve esa pregunta. Eso depende del manejador de solicitudes.

Eso ya es común en OOP durante mucho tiempo. Muchas veces creando piezas de código como:

I_Dosomething x = new Impl_Dosomething();

El inconveniente es que la clase de implementación todavía está codificada, por lo tanto, tiene el conocimiento de qué implementación se usa. DI lleva el diseño por interfaz un paso más allá, que lo único que el front end necesita saber es el conocimiento de la interfaz. Entre el DYI y el DI está el patrón de un localizador de servicios, porque el front end tiene que proporcionar una clave (presente en el registro del localizador de servicios) para permitir que su solicitud se resuelva. Ejemplo de localizador de servicio:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI ejemplo:

I_Dosomething x = DIContainer.returnThat();

Uno de los requisitos de DI es que el contenedor debe poder averiguar qué clase es la implementación de qué interfaz. Por lo tanto, un contenedor DI requiere un diseño fuertemente tipado y solo una implementación para cada interfaz al mismo tiempo. Si necesita más implementaciones de una interfaz al mismo tiempo (como una calculadora), necesita el localizador de servicios o el patrón de diseño de fábrica.

D (b) I: Inyección de dependencia y diseño por interfaz. Sin embargo, esta restricción no es un gran problema práctico. El beneficio de usar D (b) I es que sirve de comunicación entre el cliente y el proveedor. Una interfaz es una perspectiva sobre un objeto o un conjunto de comportamientos. Lo último es crucial aquí.

Prefiero la administración de contratos de servicio junto con D (b) I en la codificación. Deberían ir juntos. El uso de D (b) I como una solución técnica sin la administración organizacional de los contratos de servicios no es muy beneficioso en mi punto de vista, porque DI es entonces solo una capa adicional de encapsulación. Pero cuando puede usarlo junto con la administración de la organización, realmente puede hacer uso del principio de organización que D (b) I ofrece. A la larga, puede ayudarlo a estructurar la comunicación con el cliente y otros departamentos técnicos en temas como pruebas, versiones y desarrollo de alternativas. Cuando tienes una interfaz implícita como en una clase codificada, entonces es mucho menos comunicable con el tiempo que cuando la haces explícita usando D (b) I. Todo se reduce al mantenimiento, que se realiza con el tiempo y no a la vez. :-)


1
"El inconveniente es que la clase de implementación todavía está codificada" <- la mayoría de las veces solo hay una implementación y, como dije, no puedo pensar en código que no sea un juego que requiera una interfaz que no esté integrada (.NET )

@ acidzombie24 Puede ser ... pero compare el esfuerzo para implementar una solución usando DI desde el principio con el esfuerzo de cambiar una solución que no sea DI más adelante si necesita interfaces. Casi siempre voy con la primera opción. Es mejor pagar ahora 100 $ en lugar de tener que pagar 100.000 $ mañana.
Golo Roden

1
@GoloRoden De hecho, el mantenimiento es el tema clave utilizando técnicas como D (b) I. Eso es el 80% de los costos de una aplicación. Un diseño en el que el comportamiento requerido se hace explícito utilizando interfaces desde el principio ahorra a la organización más tarde mucho tiempo y dinero.
Loek Bergman

No voy a entender realmente hasta que tenga que pagarlo porque hasta ahora pagué $ 0 y hasta ahora solo necesito pagar $ 0. Pero pago $ 0.05 para mantener cada línea o función limpia.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.