¿Por qué debo usar la inyección de dependencia?

92

Me está costando mucho encontrar recursos sobre por qué debería usar inyección de dependencia . La mayoría de los recursos que veo explican que simplemente pasa una instancia de un objeto a otra instancia de un objeto, pero ¿por qué? ¿Esto es solo para una arquitectura / código más limpio o afecta el rendimiento en general?

¿Por qué debería hacer lo siguiente?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

En lugar de lo siguiente?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}
    
pregunta Daniel 13.11.2018 - 09:04

9 respuestas

104

La ventaja es que sin la inyección de dependencia, su clase de perfil

  • necesita saber cómo crear un objeto de configuración (viola el principio de responsabilidad única)
  • Siempre crea su objeto de configuración de la misma manera (crea un acoplamiento apretado entre los dos)

Pero con inyección de dependencia

  • La lógica para crear objetos de configuración está en otra parte
  • Es fácil usar diferentes tipos de objetos de configuración

Esto puede parecer (o incluso ser) irrelevante en este caso particular, pero imagínese si no estamos hablando de un objeto de Configuración, sino de un objeto DataStore, que puede tener diferentes implementaciones, una que almacena datos en archivos y otra que Lo almacena en una base de datos. Y para las pruebas automatizadas, también desea una implementación simulada. Ahora realmente no desea que la clase de perfil codifique de forma rígida la que utiliza, y lo que es más importante, realmente realmente, realmente no desea que la clase de perfil conozca las rutas del sistema de archivos, las conexiones de DB y las contraseñas , por lo que la creación de objetos DataStore tiene que suceda en otro lugar.

    
respondido por el Michael Borgwardt 13.11.2018 - 10:28
60

La inyección de dependencia hace que su código sea más fácil de probar.

Aprendí esto de primera mano cuando tuve la tarea de solucionar un error difícil de detectar en la integración de PayPal de Magento.

Surgiría un problema cuando PayPal le informaba a Magento sobre un pago fallido: Magento no registraría la falla correctamente.

Probar una posible solución "manualmente" sería muy tedioso: tendría que activar de alguna manera una notificación de PayPal "Fallida". Tendría que enviar un cheque electrónico, cancelarlo y esperar a que se produzca un error. ¡Eso significa más de 3 días para probar un cambio de código de un carácter!

Afortunadamente, parece que los desarrolladores del núcleo de Magento que desarrollaron esta función tenían en mente las pruebas y usaron un patrón de inyección de dependencia para hacerlo trivial. Esto nos permite verificar nuestro trabajo con un caso de prueba simple como este:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Estoy seguro de que el patrón DI tiene muchas otras ventajas, pero una mejor capacidad de prueba es el mayor beneficio en mi mente .

Si tiene curiosidad acerca de la solución a este problema, consulte el repositorio de GitHub aquí: enlace

    
respondido por el Eric Seastrand 13.11.2018 - 15:38
26

¿Por qué (cuál es el problema)?

  

¿Por qué debería usar la inyección de dependencia?

El mejor mnemónico que encontré para esto es " new is glue ": Cada vez que uses new en tu Código, ese código está vinculado a esa implementación específica . Si usa repetidamente nuevos en constructores, creará una cadena de implementaciones específicas . Y como no puede "tener" una instancia de una clase sin construirla, no puede separar esa cadena.

Como ejemplo, imagina que estás escribiendo un videojuego de carreras. Comenzó con una clase Game , que crea un RaceTrack , que crea un 8 Cars , y cada uno crea un Motor . Ahora, si desea 4 Cars adicionales con una aceleración diferente, tendrá que cambiar todas las clases mencionadas , excepto tal vez Game .

Código de limpieza

  

Es solo para una arquitectura / código más limpio

Sí .

Sin embargo, podría parecer menos claro en esta situación, porque es más un ejemplo de cómo hacerlo . La ventaja real solo se muestra cuando hay varias clases involucradas y es más difícil de demostrar, pero imagina que hubieras usado DI en el ejemplo anterior. El código que crea todas esas cosas podría verse así:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Ahora se pueden agregar esos 4 autos diferentes agregando estas líneas:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • No fueron necesarios cambios en RaceTrack , Game , Car o Motor , lo que significa que podemos estar 100% seguros de que no introdujimos ningún error nuevo allí.
  • En lugar de tener que saltar entre varios archivos, podrá ver el cambio completo en su pantalla al mismo tiempo. Este es el resultado de ver la creación / configuración / configuración como una responsabilidad no es la tarea de un automóvil construir un motor.

Consideraciones de rendimiento

  

¿O esto afecta el rendimiento en general?

No . Pero para ser completamente honesto contigo, podría.

Sin embargo, incluso en ese caso, es una cantidad tan ridículamente pequeña que no necesita preocuparse. Si en algún momento en el futuro, tiene que escribir código para un tamagotchi con el equivalente de 5Mhz CPU y 2MB RAM, entonces tal vez usted podría tener que preocuparse por esto.

En el 99,999% * de los casos, tendrá un mejor rendimiento, ya que dedicó menos tiempo a corregir errores y más tiempo a mejorar sus algoritmos de gran cantidad de recursos.

* número completamente compuesto

Información agregada: "codificada"

No se equivoque, este es todavía está muy "codificado": los números se escriben directamente en el código. No codificado de forma rígida significaría algo así como almacenar esos valores en un archivo de texto, por ejemplo, en formato JSON, y luego leerlos de ese archivo.

Para hacer eso, debes agregar código para leer un archivo y luego analizar JSON. Si consideras el ejemplo de nuevo; en la versión no DI, un Car o un Motor ahora tienen que leer un archivo. Eso no parece que tenga demasiado sentido.

En la versión DI, lo agregarías al código que configura el juego.

    
respondido por el R. Schmitz 13.11.2018 - 18:45
16

Siempre me desconcertó la inyección de dependencia. Parecía que solo existía dentro de las esferas de Java, pero esas esferas hablaban de ello con gran reverencia. Fue uno de los grandes Patrones, se ve, que se dice que trae orden al caos. Pero los ejemplos siempre fueron complicados y artificiales, estableciendo un problema y luego resolviéndolo haciendo el código más complicado.

Tenía más sentido cuando un compañero desarrollador de Python me impartió esta sabiduría: es solo pasar argumentos a las funciones. Es apenas un patrón en absoluto; más bien como un recordatorio de que puede pedir algo como un argumento, incluso si podría haber proporcionado un valor razonable usted mismo.

Entonces, tu pregunta es aproximadamente equivalente a "¿por qué debería mi función tomar argumentos?" y tiene muchas de las mismas respuestas, a saber: para permitir que la persona que llama tome decisiones .

Esto conlleva un costo, por supuesto, porque ahora está forzando a la persona que llama a tomar alguna decisión (a menos que haga el argumento opcional), y la interfaz Es algo más complejo. A cambio, ganas flexibilidad.

Entonces: ¿hay alguna buena razón por la que necesite utilizar específicamente este tipo / valor Setting ? ¿Existe una buena razón para que el código de llamada desee un tipo / valor Setting diferente? (Recuerde, las pruebas son código !)

    
respondido por el Eevee 14.11.2018 - 17:44
7

El ejemplo que da no es la inyección de dependencia en el sentido clásico. La inyección de dependencia generalmente se refiere a pasar objetos en un constructor o al usar "inyección de establecimiento" justo después de que se crea el objeto, para establecer un valor en un campo en un objeto creado recientemente.

Su ejemplo pasa un objeto como argumento a un método de instancia. Este método de instancia luego modifica un campo en ese objeto. ¿Inyección de dependencia? ¿Rompiendo la encapsulación y ocultando datos? Absolutamente!

Ahora, si el código era así:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Entonces diría que está utilizando la inyección de dependencia. La diferencia notable es que un objeto Settings se pasa al constructor o un objeto Profile .

Esto es útil si el objeto de Configuración es costoso o complejo de construir, o la Configuración es una interfaz o clase abstracta donde existen múltiples implementaciones concretas para cambiar el comportamiento del tiempo de ejecución.

Ya que está accediendo directamente a un campo en el objeto de Configuración en lugar de llamar a un método, no puede aprovechar el Polimorfismo, que es uno de los beneficios de la inyección de dependencia.

Parece que la configuración de un perfil es específica para ese perfil. En este caso yo haría uno de los siguientes:

  1. Cree una instancia del objeto de configuración dentro del constructor de perfiles

  2. Pase el objeto de Configuración en el constructor y copie sobre campos individuales que se aplican al Perfil

Honestamente, al pasar el objeto de Configuración a deactivateProfile y luego modificar un campo interno del objeto de Configuración es un olor de código. El objeto Configuración debe ser el único que modifique sus campos internos.

    
respondido por el Greg Burghardt 13.11.2018 - 15:00
7

Sé que voy a llegar tarde a esta fiesta, pero siento que se está perdiendo un punto importante.

  

¿Por qué debería hacer esto?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

No deberías. Pero no porque la inyección de dependencia es una mala idea. Es porque esto lo está haciendo mal.

Veamos estas cosas usando código. Vamos a hacer esto:

$profile = new Profile();
$profile->deactivateProfile($setting);

cuando obtengamos lo mismo de esto:

$setting->isActive = false; // Deactivate profile

Por supuesto que parece una pérdida de tiempo. Es cuando lo haces de esta manera. Este no es el mejor uso de la inyección de dependencia. Ni siquiera es el mejor uso de una clase.

Ahora qué pasaría si tuviéramos esto:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

Y ahora application es libre de activar y desactivar profile sin tener que saber nada en particular sobre el setting que realmente está cambiando. ¿Por qué es eso bueno? En caso de que necesite cambiar la configuración. El application está excluido de esos cambios, por lo que puedes volverte loco en un espacio seguro sin tener que ver cómo se rompe todo tan pronto como tocas algo.

Esto sigue el construcción separada del comportamiento . El patrón DI aquí es simple. Construya todo lo que necesita en el nivel más bajo posible, conéctelos y luego comience a marcar todo el comportamiento con una sola llamada.

El resultado es que tienes un lugar separado para decidir qué se conecta con qué y un lugar diferente para administrar lo que dice qué a lo que sea.

Intente eso con algo que tenga que mantener a lo largo del tiempo y vea si no ayuda.

    
respondido por el candied_orange 13.11.2018 - 22:06
6

Como cliente, cuando contrata a un mecánico para que le haga algo a su automóvil, ¿espera que ese mecánico construya un automóvil desde cero solo para luego trabajar con él? No, usted le da al mecánico el automóvil en el que quiere que trabaje .

Como propietario del garaje, cuando le indica a un mecánico que le haga algo a un automóvil, ¿espera que el mecánico cree su propio destornillador / llave / piezas de automóvil? No, usted le proporciona al mecánico las piezas / herramientas que necesita usar

¿Por qué hacemos esto? Bueno, piensalo. Eres el propietario de un garaje que quiere contratar a alguien para que se convierta en tu mecánico. Les enseñarás a ser mecánicos (= escribirás el código).

Lo que va a ser más fácil:

  • Enseñe a un mecánico cómo colocar un spoiler en un automóvil con un destornillador.
  • Enseña a un mecánico a crear un automóvil, crea un spoiler, crea un destornillador y luego adjunte el spoiler recién creado al automóvil recién creado con el destornillador recién creado.

El hecho de que su mecánico no cree todo desde cero tiene grandes beneficios:

  • Obviamente, la capacitación (= desarrollo) se reduce drásticamente si solo le proporciona a su mecánico las herramientas y piezas existentes.
  • Si el mismo mecánico tiene que realizar el mismo trabajo varias veces, puede asegurarse de que reutiliza el destornillador en lugar de tirar siempre el viejo y crear uno nuevo.
  • Además, un mecánico que haya aprendido a crear todo tendrá que ser mucho más experto y, por lo tanto, esperará un salario más alto. La analogía de codificación aquí es que una clase con muchas responsabilidades es mucho más difícil de mantener que una clase con una única responsabilidad estrictamente definida.
  • Además, cuando nuevos inventos llegan al mercado y los spoilers ahora se están fabricando de carbono en lugar de plástico; tendrá que volver a entrenar (= volver a desarrollar) a su mecánico experto. Pero su mecánico "simple" no tendrá que volver a entrenarse mientras el alerón aún pueda acoplarse de la misma manera.
  • Tener un mecánico que no confía en un automóvil que ellos mismos han construido significa que usted tiene un mecánico que puede manejar cualquier automóvil que puedan recibir. Incluyendo autos que ni siquiera existían al momento de entrenar al mecánico. Sin embargo, su mecánico experto no podrá construir autos más nuevos que hayan sido creados después de su entrenamiento.

Si contrata y capacita al mecánico experto, va a terminar con un empleado que cuesta más, toma más tiempo para realizar lo que debería ser un trabajo simple y siempre tendrá que volver a capacitarse cada vez que uno de sus muchas responsabilidades deben actualizarse.

La analogía del desarrollo es que si usa clases con dependencias codificadas, entonces va a terminar con clases difíciles de mantener que necesitarán redesarrollo / cambios continuos cada vez que haya una nueva versión del objeto ( Settings en su caso ) se crea y tendrá que desarrollar una lógica interna para que la clase tenga la capacidad de crear diferentes tipos de objetos Settings .

Además, quienquiera que consuma su clase ahora también tendrá que preguntar a la clase para crear el objeto Settings correcto, en lugar de simplemente poder pasar la clase a cualquier objeto Settings quiere pasar. Esto significa un desarrollo adicional para que el consumidor descubra cómo pedir a la clase que cree la herramienta correcta.

Sí, la inversión de dependencia requiere un poco más de esfuerzo para escribir en lugar de codificar la dependencia. Sí, es molesto tener que escribir más.

Pero ese es el mismo argumento que elegir valores literales de código duro porque "declarar variables requiere más esfuerzo". Técnicamente correcto, pero los profesionales superan los inconvenientes en varios órdenes de magnitud .

El beneficio de la inversión de dependencia no se experimenta cuando crea la primera versión de la aplicación. El beneficio de la inversión de dependencia se experimenta cuando necesita cambiar o extender esa versión inicial. Y no se engañe pensando que lo logrará bien la primera vez y no necesitará extender / cambiar el código. Tendrás que cambiar las cosas.

  

¿Esto afecta el rendimiento en su conjunto?

Esto no afecta el rendimiento en tiempo de ejecución de la aplicación. Pero tiene un impacto masivo en el tiempo de desarrollo (y, por lo tanto, en el rendimiento) del desarrollador .

    
respondido por el Flater 13.11.2018 - 10:51
0

Debes usar técnicas para resolver los problemas que son buenos para resolver cuando tienes esos problemas. La inversión de dependencia y la inyección no son diferentes.

La inversión o inyección de dependencia es una técnica que permite que su código decida a qué implementación de un método se llama en tiempo de ejecución. Esto maximiza los beneficios de la unión tardía. La técnica es necesaria cuando el lenguaje no admite el reemplazo en tiempo de ejecución de funciones que no son de instancia. Por ejemplo, Java carece de un mecanismo para reemplazar las llamadas a un método estático con llamadas a una implementación diferente; contrasta con Python, donde todo lo que es necesario para reemplazar la llamada a la función es vincular el nombre a una función diferente (reasignar la variable que contiene la función).

¿Por qué queremos variar la implementación de la función? Hay dos razones principales:

  • Queremos usar falsificaciones para propósitos de prueba. Esto nos permite probar una clase que depende de una búsqueda de base de datos sin estar realmente conectado a la base de datos.
  • Necesitamos soportar implementaciones múltiples. Por ejemplo, podríamos necesitar configurar un sistema que admita bases de datos MySQL y PostgreSQL.

Es posible que también desee tomar nota de la inversión de los contenedores de control. Esta es una técnica que pretende ayudarlo a evitar árboles de construcción enormes y enredados que se parecen a este pseudocódigo:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Le permite registrar sus clases y luego hace la construcción por usted:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

Tenga en cuenta que es más sencillo si las clases registradas pueden ser singletons sin estado .

Palabra de advertencia

Tenga en cuenta que la inversión de dependencia no debe ser su respuesta a la lógica de desacoplamiento. Busque oportunidades para utilizar parametrización en su lugar. Considere este método de pseudocódigo, por ejemplo:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Podríamos usar la inversión de dependencia para algunas partes de este método:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Pero no deberíamos, al menos no completamente. Tenga en cuenta que hemos creado una clase con estado con Querier . Ahora contiene una referencia a algún objeto de conexión esencialmente global. Esto crea problemas, como la dificultad para comprender el estado general del programa y cómo las diferentes clases se coordinan entre sí. Tenga en cuenta también que nos vemos obligados a falsificar el buscador o la conexión si queremos probar la lógica de promediado. Un mejor enfoque sería aumentar la parametrización :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

Y la conexión se administraría a un nivel aún más alto que es responsable de la operación en su conjunto y sabe qué hacer con esta salida.

Ahora podemos probar la lógica de promedios de manera completamente independiente de la consulta, y lo que es más, podemos usarla en una variedad más amplia de situaciones. Podríamos preguntarnos si incluso necesitamos los objetos MyQuerier y Averager , y tal vez la respuesta sea que no lo hacemos si no pretendemos realizar la prueba unitaria StuffDoer , y no la prueba unitaria StuffDoer sería perfecta. razonable, ya que está tan estrechamente acoplado a la base de datos. Podría tener más sentido simplemente dejar que las pruebas de integración lo cubran. En ese caso, podríamos estar bien haciendo fetchAboveMin y averageData en métodos estáticos.

    
respondido por el jpmc26 13.11.2018 - 16:56
0

Como todos los patrones, es muy válido preguntar "por qué" para evitar diseños abultados.

Para la inyección de dependencia, esto se ve fácilmente al pensar en las dos facetas más importantes del diseño OOP ...

Acoplamiento bajo

Acoplamiento en la programación de computadoras :

  

En ingeniería de software, el acoplamiento es el grado de interdependencia entre los módulos de software; una medida de cuán estrechamente conectadas están dos rutinas o módulos; La fuerza de las relaciones entre los módulos.

Desea lograr un acoplamiento bajo . Dos cosas que están fuertemente acopladas significa que si cambias una, es muy probable que tengas que cambiar la otra. Errores o restricciones en uno probablemente inducirán errores / restricciones en el otro; y así.

Una clase que ejemplifica objetos de las otras es un acoplamiento muy fuerte, porque la una necesita saber acerca de la existencia de la otra; necesita saber cómo instanciarlo (qué argumentos necesita el constructor), y esos argumentos deben estar disponibles cuando se llama al constructor. Además, dependiendo de si el lenguaje necesita deconstrucción explícita (C ++), esto introducirá más complicaciones. Si introduce nuevas clases (es decir, un NextSettings o lo que sea), debe volver a la clase original y agregar más llamadas a sus constructores.

Alta cohesión

Cohesion :

  

En la programación de computadoras, la cohesión se refiere al grado en que los elementos dentro de un módulo pertenecen juntos.

Este es el otro lado de la moneda. Si observa una unidad de código (un método, una clase, un paquete, etc.), desea tener todo el código dentro de esa unidad para tener la menor cantidad de responsabilidades posible.

Un ejemplo básico para esto sería el patrón MVC: separa claramente el modelo de dominio de la vista (GUI) y una capa de control que los une.

Esto evita la expansión de código donde se obtienen trozos grandes que hacen muchas cosas diferentes; Si desea cambiar alguna parte del mismo, también debe realizar un seguimiento de todas las demás funciones, intentando evitar errores, etc .; y rápidamente te programas en un agujero donde es muy difícil salir.

Con la inyección de dependencia, usted delega la creación o el seguimiento de las dependencias a las clases (o archivos de configuración) que implementan su DI. A otras clases no les importará mucho lo que está sucediendo exactamente; estarán trabajando con algunas interfaces genéricas, y no tienen idea de lo que es la implementación real, lo que significa que no son responsables de las otras cosas.

    
respondido por el AnoE 15.11.2018 - 08:02

Lea otras preguntas en las etiquetas