Comprender la inyección de dependencia


109

Estoy leyendo sobre la inyección de dependencia (DI). Para mí, es algo muy complicado de hacer, ya que lo estaba leyendo también hacía referencia a la inversión de control (IoC) y sentí que iba a emprender un viaje.

Este es mi entendimiento: en lugar de crear un modelo en la clase que también lo consume, pasa (inyecta) el modelo (ya lleno de propiedades interesantes) a donde se necesita (a una nueva clase que podría tomarlo como parámetro en El constructor).

Para mí, esto es solo pasar una discusión. Debo haber perdido la comprensión del punto? ¿Tal vez se vuelve más obvio con proyectos más grandes?

Entiendo que no es DI (usando pseudocódigo):

public void Start()
{
    MyClass class = new MyClass();
}

...

public MyClass()
{
    this.MyInterface = new MyInterface(); 
}

Y DI sería

public void Start()
{
    MyInterface myInterface = new MyInterface();
    MyClass class = new MyClass(myInterface);
}

...

public MyClass(MyInterface myInterface)
{
    this.MyInterface = myInterface; 
}

¿Podría alguien arrojar algo de luz ya que estoy seguro de que estoy en un lío aquí?


55
Intente tener 5 de MyInterface y MyClass Y tenga varias clases que formen una cadena de dependencia. Se convierte en un dolor en el trasero para configurar todo.
Eufórico

159
" Para mí, esto es solo pasar una discusión. ¿Debo haber entendido mal el punto? " Lo tienes. Eso es "inyección de dependencia". Ahora vea qué otra jerga loca puede encontrar para conceptos simples, ¡es divertido!
Eric Lippert

8
No puedo votar lo suficiente el comentario del señor Lippert. También encontré que era una jerga confusa para algo que estaba haciendo naturalmente de todos modos.
Alex Humphrey

16
@EricLippert: Si bien es correcto, creo que es un poco reductor. Parameters-as-DI no es un concepto directo e inmediato para su programador promedio imperativo / OO, que está acostumbrado a pasar datos. DI aquí le permite transmitir el comportamiento , lo que requiere una visión del mundo más flexible que la que tendrá un programador mediocre.
Phoshi

14
@Phoshi: Usted hace un buen punto, pero también ha señalado por qué soy escéptico de que "inyección de dependencia" sea una buena idea en primer lugar. Si escribo una clase que depende de su corrección y rendimiento del comportamiento de otra clase, ¡lo último que quiero hacer es hacer que el usuario de la clase sea responsable de construir y configurar correctamente la dependencia! Ese es mi trabajo Cada vez que deja que un consumidor inyecte una dependencia, crea un punto en el que el consumidor podría hacerlo mal.
Eric Lippert

Respuestas:


106

Bueno, sí, inyectas tus dependencias, ya sea a través de un constructor o de propiedades.
Una de las razones para esto es no gravar MyClass con los detalles de cómo se debe construir una instancia de MyInterface. MyInterface podría ser algo que tiene una lista completa de dependencias por sí mismo y el código de MyClass se volvería feo muy rápido si hubiera creado una instancia de todas las dependencias de MyInterface dentro de MyClass.

Otra razón es para la prueba.
Si tiene una dependencia en una interfaz de lector de archivos e inyecta esta dependencia a través de un constructor en, por ejemplo, ConsumerClass, eso significa que durante la prueba, puede pasar una implementación en memoria del lector de archivos a ConsumerClass, evitando la necesidad de hacer E / S durante las pruebas.


13
Esto es genial, bien explicado. ¡Acepta un +1 imaginario ya que mi representante aún no lo permitirá!
MyDaftQuestions

11
El complicado escenario de construcción es un buen candidato para usar el patrón Factory. De esa manera, la fábrica asume la responsabilidad de construir el objeto, de modo que la clase en sí misma no tenga que hacerlo y usted se apegue al principio de responsabilidad única.
Matt Gibson

1
@MattGibson buen punto.
Stefan Billiet

2
+1 para las pruebas, una DI bien estructurada ayudará a acelerar las pruebas y a realizar las pruebas unitarias contenidas en la clase que está probando.
Umur Kontacı

52

La forma en que se puede implementar DI depende mucho del lenguaje utilizado.

Aquí hay un ejemplo simple que no es DI:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo() {
        bar = new Bar();
        qux = new Qux();
    }
}

Esto apesta, por ejemplo, para una prueba para la que quiero usar un objeto simulado bar. Entonces podemos hacer esto más flexible y permitir que las instancias se pasen a través del constructor:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo(Bar bar, Qux qux) {
        this.bar = bar;
        this.qux = qux;
    }
}

// in production:
new Foo(new Bar(), new Qux());
// in test:
new Foo(new BarMock(), new Qux());

Esta ya es la forma más simple de inyección de dependencia. Pero esto todavía apesta porque todo tiene que hacerse manualmente (también, porque la persona que llama puede contener una referencia a nuestros objetos internos y, por lo tanto, invalidar nuestro estado).

Podemos introducir más abstracción usando fábricas:

  • Una opción es que la Foogenere una fábrica abstracta

    interface FooFactory {
        public Foo makeFoo();
    }
    
    class ProductionFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new Bar(), new Baz()) }
    }
    
    class TestFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new BarMock(), new Baz()) }
    }
    
    FooFactory fac = ...; // depends on test or production
    Foo foo = fac.makeFoo();
    
  • Otra opción es pasar una fábrica al constructor:

    interface DependencyManager {
        public Bar makeBar();
        public Qux makeQux();
    }
    class ProductionDM implements DependencyManager {
        public Bar makeBar() { return new Bar() }
        public Qux makeQux() { return new Qux() }
    }
    class TestDM implements DependencyManager {
        public Bar makeBar() { return new BarMock() }
        public Qux makeQux() { return new Qux() }
    }
    
    class Foo {
        private Bar bar;
        private Qux qux;
    
        public Foo(DependencyManager dm) {
            bar = dm.makeBar();
            qux = dm.makeQux();
        }
    }
    

Los problemas restantes con esto son que necesitamos escribir una nueva DependencyManagersubclase para cada configuración, y que el número de dependencias que se pueden gestionar es bastante restringido (cada nueva dependencia necesita un nuevo método en la interfaz).

Con características como la reflexión y la carga dinámica de clases, podemos evitar esto. Pero esto depende mucho del idioma utilizado. En Perl, las clases pueden ser referenciadas por su nombre, y yo podría simplemente hacer

package Foo {
    use signatures;

    sub new($class, $dm) {
        return bless {
            bar => $dm->{bar}->new,
            qux => $dm->{qux}->new,
        } => $class;
    }
}

my $prod = { bar => 'My::Bar', qux => 'My::Qux' };
my $test = { bar => 'BarMock', qux => 'QuxMock' };
$test->{bar} = 'OtherBarMock';  # change conf at runtime

my $foo = Foo->new(rand > 0.5 ? $prod : $test);

En lenguajes como Java, podría hacer que el administrador de dependencias se comportara de manera similar a Map<Class, Object>:

Bar bar = dm.make(Bar.class);

A qué clase real Bar.classse resuelve se puede configurar en tiempo de ejecución, por ejemplo, manteniendo un Map<Class, Class>mapa que asigne las interfaces a las implementaciones.

Map<Class, Class> dependencies = ...;

public <T> T make(Class<T> c) throws ... {
    // plus a lot more error checking...
    return dependencies.get(c).newInstance();
}

Todavía hay un elemento manual involucrado en la escritura del constructor. Pero podemos hacer que el constructor sea completamente innecesario, por ejemplo, conduciendo el DI a través de anotaciones:

class Foo {
    @Inject(Bar.class)
    private Bar bar;

    @Inject(Qux.class)
    private Qux qux;

    ...
}

dm.make(Foo.class);  // takes care of initializing "bar" and "qux"

Aquí hay un ejemplo de implementación de un marco DI pequeño (y muy restringido): http://ideone.com/b2ubuF , aunque esta implementación es completamente inutilizable para objetos inmutables (esta implementación ingenua no puede tomar ningún parámetro para el constructor).


77
Esto es excelente con excelentes ejemplos, aunque me tomará algunas veces leerlo para digerirlo todo, pero gracias por tomarme tanto tiempo.
MyDaftQuestions

33
Es divertido cómo cada próxima simplificación es más compleja
Kromster

48

Nuestros amigos en Stack Overflow tienen una buena respuesta a esto . Mi favorita es la segunda respuesta, incluida la cita:

La "inyección de dependencia" es un término de 25 dólares para un concepto de 5 centavos. (...) La inyección de dependencia significa dar a un objeto sus variables de instancia. (...)

del blog de James Shore . Por supuesto, hay versiones / patrones más complejos en capas además de esto, pero es suficiente para entender básicamente lo que está sucediendo. En lugar de que un objeto cree sus propias variables de instancia, se pasan desde afuera.


10
Había leído esto ... y hasta ahora, no tenía sentido ... Ahora, tiene sentido. La dependencia no es realmente un concepto tan grande de entender (independientemente de lo poderoso que sea), pero tiene un gran nombre y mucho ruido de Internet asociado.
MyDaftQuestions

18

Digamos que está haciendo el diseño de interiores en su sala de estar: instala una lámpara de araña elegante en el techo y conecta una elegante lámpara de pie a juego. Un año después, su encantadora esposa decide que le gustaría que la habitación fuera un poco menos formal. ¿Qué dispositivo de iluminación será más fácil de cambiar?

La idea detrás de DI es utilizar el enfoque de "plug-in" en todas partes. Esto puede no parecer un gran problema si vives en una cabaña simple sin paneles de yeso y todos los cables eléctricos expuestos. (Eres un manitas, ¡puedes cambiar cualquier cosa!) Y de manera similar, en una aplicación pequeña y simple, DI puede agregar más complicaciones de lo que vale.

Pero para una aplicación grande y compleja, la DI es indispensable para el mantenimiento a largo plazo. Le permite "desconectar" módulos completos de su código (el backend de la base de datos, por ejemplo), e intercambiarlos con diferentes módulos que logran lo mismo pero lo hacen de una manera completamente diferente (un sistema de almacenamiento en la nube, por ejemplo).

Por cierto, una discusión sobre DI estaría incompleta sin una recomendación de Dependency Injection en .NET , por Mark Seeman. Si está familiarizado con .NET y alguna vez tiene la intención de participar en el desarrollo de software a gran escala, esta es una lectura esencial. Él explica el concepto mucho mejor que yo.

Permítame dejarlo con una última característica esencial de DI que su ejemplo de código pasa por alto. DI le permite aprovechar el vasto potencial de flexibilidad que está encapsulado en el adagio " Programa para interfaces ". Para modificar su ejemplo ligeramente:

public void Main()
{
    ILightFixture fixture = new ClassyChandelier();
    MyRoom room = new MyRoom (fixture);
}
...
public MyRoom(ILightFixture fixture)
{
    this.MyLightFixture = fixture ; 
}

Pero AHORA, debido a que MyRoomestá diseñado para la interfaz ILightFixture , puedo ingresar fácilmente el próximo año y cambiar una línea en la función Principal ("inyectar" una "dependencia" diferente), e instantáneamente obtengo una nueva funcionalidad en MyRoom (sin tener que reconstruya o vuelva a implementar MyRoom, si sucede que está en una biblioteca separada. Esto supone, por supuesto, que todas sus implementaciones de ILightFixture están diseñadas teniendo en cuenta la sustitución de Liskov). Todo, con solo un simple cambio:

public void Main()
{
    ILightFixture fixture = new MuchLessFormalLightFixture();
    MyRoom room = new MyRoom (fixture);
}

Además, ahora puedo hacer una prueba unitaria MyRoom(usted es una prueba unitaria, ¿verdad?) Y usar una lámpara "simulada", que me permite realizar pruebas MyRoomcompletamente independientes deClassyChandelier.

Hay muchas más ventajas, por supuesto, pero estas son las que me vendieron la idea.


Quería decir gracias, esto fue útil para mí, pero no quieren que hagas eso, sino que usan comentarios para sugerir mejoras, por lo que, si lo deseas, puedes agregar una de las otras ventajas que mencionaste. Pero por lo demás, gracias, esto fue útil para mí.
Steve

2
También estoy interesado en el libro al que hizo referencia, pero me parece alarmante que haya un libro de 584 páginas sobre este tema.
Steve

4

La inyección de dependencia solo está pasando un parámetro, pero eso todavía conduce a algunos problemas, y el nombre está destinado a centrarse un poco en por qué la diferencia entre crear un objeto y pasar uno es importante.

Es importante porque (obviamente) cualquier objeto A llama al tipo B, A depende de cómo funciona B. Lo que significa que si A toma la decisión específica de qué tipo concreto de B usará, entonces se pierde mucha flexibilidad en cómo A puede ser usado por clases externas, sin modificar A.

Menciono esto porque hablas de "perder el punto" de la inyección de dependencia, y este es en gran medida el motivo por el cual a la gente le gusta hacerlo. Puede significar simplemente pasar un parámetro, pero si lo hace puede ser importante.

Además, parte de la dificultad en la implementación real de DI se muestra mejor en grandes proyectos. Por lo general, moverá la decisión real de qué clases concretas usar hasta el final, por lo que su método de inicio podría verse así:

public void Start()
{
    MySubSubInterfaceA mySubSubInterfaceA = new mySubSubInterfaceA();
    MySubSubInterfaceB mySubSubInterfaceB = new mySubSubInterfaceB();     
    MySubInterface mySubInterface = new MySubInterface(mySubSubInterfaceA,mySubSubInterfaceB);
    MyInterface myInterface = new MyInterface(MySubInterface);
    MyClass class = new MyClass(myInterface);
}

pero con otras 400 líneas de este tipo de código fascinante. Si imagina mantener eso con el tiempo, el atractivo de los contenedores DI se hace más evidente.

Además, imagine una clase que, por ejemplo, implemente IDisposable. Si las clases que lo usan lo obtienen mediante inyección en lugar de crearlo ellos mismos, ¿cómo saben cuándo llamar a Dispose ()? (Que es otro problema que trata un contenedor DI).

Entonces, claro, la inyección de dependencia es solo pasar un parámetro, pero realmente cuando las personas dicen inyección de dependencia, significan "pasar un parámetro a su objeto versus la alternativa de hacer que su objeto cree una instancia de algo en sí mismo", y lidiar con todos los beneficios de los inconvenientes de tal enfoque, posiblemente con la ayuda de un contenedor DI.


¡Eso es un montón de subs e interfaces! ¿Te refieres a actualizar una interfaz en lugar de una clase que implementa la interfaz? Parece mal
JBRWilkinson

@JBRWilkinson Me refiero a una clase que implementa una interfaz. Debería aclarar eso. Sin embargo, el punto es que una aplicación grande tendrá clases con dependencias que a su vez tienen dependencias que a su vez tienen dependencias ... Configurarlo todo manualmente en el método Main es el resultado de hacer muchas inyecciones de constructor.
psr

4

A nivel de clase, es fácil.

La 'Inyección de dependencia' simplemente responde la pregunta "¿cómo voy a encontrar a mis colaboradores" con "te presionan? No tienes que ir a buscarlos tú mismo". (Es similar a 'Inversión de control', pero no lo mismo, donde la pregunta "¿cómo ordenaré mis operaciones sobre mis entradas?" Tiene una respuesta similar).

El único beneficio de que sus colaboradores lo presionen es que permite que el código del cliente use su clase para componer un gráfico de objetos que se adapte a sus necesidades actuales ... no ha predeterminado arbitrariamente la forma y la mutabilidad del gráfico al decidir en privado Los tipos concretos y el ciclo de vida de sus colaboradores.

(Todos esos otros beneficios, de comprobabilidad, de acoplamiento flojo, etc., se derivan en gran medida del uso de interfaces y no tanto de la dependencia-inyección-ness, aunque DI promueve naturalmente el uso de interfaces).

Vale la pena señalar que, si no evitar crear instancias de sus propios colaboradores, su clase debe , por tanto, obtener sus colaboradores de un constructor, una propiedad o un método de argumento (esta última opción se suele pasar por alto, por cierto ... lo hace no siempre tiene sentido que los colaboradores de una clase formen parte de su "estado").

Y eso es algo bueno.

A nivel de aplicación ...

Esto en cuanto a la visión de las clases por clase. Supongamos que tiene un montón de clases que siguen la regla de "no crear instancias de sus propios colaboradores" y desea hacer una aplicación a partir de ellas. Lo más simple es usar un buen código antiguo (¡una herramienta realmente útil para invocar constructores, propiedades y métodos!) Para componer el gráfico de objeto que desee y arrojar algo de entrada. (Sí, algunos de esos objetos en su gráfico serán fábricas de objetos, que se han pasado como colaboradores a otros objetos de larga vida en el gráfico, listos para el servicio ... ¡no puede preconstruir cada objeto! )

... su necesidad de configurar 'flexiblemente' el gráfico de objetos de su aplicación ...

Dependiendo de sus otros objetivos (no relacionados con el código), es posible que desee otorgarle al usuario final cierto control sobre el gráfico de objeto implementado. Esto lo lleva en la dirección de un esquema de configuración, de un tipo u otro, ya sea un archivo de texto de su propio diseño con algunos pares de nombre / valor, un archivo XML, un DSL personalizado, un lenguaje declarativo de descripción gráfica como YAML, un lenguaje de secuencias de comandos imperativo como JavaScript, o algo más apropiado para la tarea en cuestión. Lo que sea necesario para componer un gráfico de objeto válido, de una manera que satisfaga las necesidades de sus usuarios.

... puede ser una fuerza de diseño significativa.

En el más extremo circunstancia de ese tipo, se puede optar por tomar un enfoque muy general, y dar a los usuarios finales un general de mecanismo de 'cableado' el objeto gráfico de su elección e incluso permitir que ellos para proporcionar realizaciones concretas de interfaces para la tiempo de ejecución! (Su documentación es una joya reluciente, sus usuarios son muy inteligentes, están familiarizados con al menos el contorno aproximado del gráfico de objetos de su aplicación, pero no tienen un compilador a mano). Este escenario puede ocurrir teóricamente en algunas situaciones de 'empresa'.

En ese caso, es probable que tenga un lenguaje declarativo que permita a sus usuarios expresar cualquier tipo, la composición de un gráfico de objetos de esos tipos y una paleta de interfaces que el usuario final mítico puede mezclar y combinar. Para reducir la carga cognitiva en sus usuarios, prefiere un enfoque de 'configuración por convención', de modo que solo tengan que intervenir y anular el fragmento de gráfico de objeto de interés, en lugar de luchar con todo.

¡Pobre diablo!

Debido a que no le gustaba escribir todo eso usted mismo (pero en serio, consulte un enlace de YAML para su idioma), está utilizando un marco DI de algún tipo.

Dependiendo de la madurez de ese marco, es posible que no tenga la opción de usar la inyección de constructor, incluso cuando tenga sentido (los colaboradores no cambian durante la vida útil de un objeto), lo que le obliga a usar Setter Injection (incluso cuando los colaboradores no cambian durante la vida útil de un objeto, e incluso cuando no hay realmente una razón lógica por la cual todas las implementaciones concretas de una interfaz deben tener colaboradores de un tipo específico). Si es así, actualmente estás en un infierno de acoplamiento fuerte, a pesar de tener diligentemente 'interfaces utilizadas' en toda tu base de código: ¡horror!

Sin embargo, con suerte, usó un marco DI que le da la opción de inyección de constructor, y sus usuarios solo están un poco malhumorados con usted por no pasar más tiempo pensando en las cosas específicas que necesitaban para configurar y dándoles una interfaz de usuario más adecuada para la tarea a mano. (Aunque para ser justos, probablemente intentaste pensar en una forma, pero JavaEE te decepcionó y tuviste que recurrir a este horrible truco).

Bootnote

En ningún momento está utilizando Google Guice, que le brinda al codificador una forma de prescindir de la tarea de componer un gráfico de objeto con código ... escribiendo código. Argh!


0

Mucha gente dijo aquí que DI es un mecanismo para externalizar la inicialización de una variable de instancia.

Para ejemplos obvios y simples, realmente no necesita "inyección de dependencia". Puede hacerlo mediante un simple parámetro que pasa al constructor, como señaló en la pregunta.

Sin embargo, creo que un mecanismo dedicado de "inyección de dependencia" es útil cuando se habla de recursos a nivel de aplicación, donde tanto la persona que llama como la persona que llama no saben nada sobre estos recursos (¡y no quieren saberlo!).

Por ejemplo: conexión de base de datos, contexto de seguridad, servicio de registro, archivo de configuración, etc.

Además, en general, cada singleton es un buen candidato para DI. Cuantos más tenga y use, más posibilidades tendrá de DI.

Para este tipo de recursos, está bastante claro que no tiene sentido moverlos a través de listas de parámetros y, por lo tanto, se inventó la DI.

Última nota, también necesita un contenedor DI, que hará la inyección real ... (nuevamente, para liberar a la persona que llama y a la persona que llama de los detalles de implementación).


-2

Si pasa un tipo de referencia abstracto, el código de llamada puede pasar cualquier objeto que extienda / implemente el tipo abstracto. Entonces, en el futuro, la clase que usa el objeto podría usar un nuevo objeto que se haya creado sin modificar su código.


-4

Esta es otra opción además de la respuesta de amon

Usar constructores:

clase Foo {
    Bar privado bar;
    Qux qux privado;

    Foo privado () {
    }

    public Foo (constructor de constructores) {
        this.bar = builder.bar;
        this.qux = builder.qux;
    }

    Generador de clase estática pública {
        Bar privado bar;
        Qux qux privado;
        public Buider setBar (Bar bar) {
            this.bar = bar;
            devuelve esto;
        }
        public Builder setQux (Qux qux) {
            this.qux = qux;
            devuelve esto;
        }
        public Foo build () {
            volver nuevo Foo (esto);
        }
    }
}

// en producción:
Foo foo = nuevo Foo.Builder ()
    .setBar (nueva barra ())
    .setQux (nuevo Qux ())
    .construir();

// en prueba:
Foo testFoo = nuevo Foo.Builder ()
    .setBar (nuevo BarMock ())
    .setQux (nuevo Qux ())
    .construir();

Esto es lo que uso para las dependencias. No uso ningún marco DI. Se interponen en el camino.


44
Esto no agrega mucho a las otras respuestas de hace un año, y no ofrece ninguna explicación de por qué un constructor podría ayudar al autor de la pregunta a comprender la inyección de dependencia.

@Snowman Es OTRA opción en lugar de DI. Lamento que no lo veas así. Gracias por el voto negativo.
user3155341

1
el autor de la pregunta quería entender si la DI realmente es tan simple como pasar un parámetro, no estaba pidiendo alternativas a la DI.

1
Los ejemplos del OP son muy buenos. Sus ejemplos proporcionan más complejidad para algo que es una idea simple. No ilustran el tema de la pregunta.
Adam Zuckerman

El constructor DI está pasando un parámetro. Excepto que está pasando un objeto de interfaz en lugar de un objeto de clase concreto. Es esta 'dependencia' de una implementación concreta que se resuelve a través de DI. El constructor no sabe qué tipo se está pasando, tampoco le importa, solo sabe que está recibiendo un tipo que implementa una interfaz. Sugiero PluralSight, tiene un excelente curso de DI / IOC para principiantes.
Hardgraf
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.