¿Cuáles son los beneficios de la inyección de dependencia en los casos en que casi todos necesitan acceso a una estructura de datos común?

20

Hay muchas razones por las cuales los globales son malvados en OOP.

Si el número o el tamaño de los objetos que necesitan compartirse es demasiado grande para pasarlo de manera eficiente en los parámetros de la función, generalmente todos recomiendan la inyección de dependencia en lugar de un objeto global.

Sin embargo, en el caso de que casi todos necesiten saber sobre una determinada estructura de datos, ¿por qué la inyección de dependencias es mejor que un objeto global?

Ejemplo (uno simplificado, para mostrar el punto en general, sin profundizar demasiado en una aplicación específica)

Hay una serie de vehículos virtuales que tienen una gran cantidad de propiedades y estados, desde tipo, nombre, color, velocidad, posición, etc. Varios usuarios pueden controlarlos de forma remota y una gran cantidad de eventos (ambos iniciado y automático) puede cambiar muchos de sus estados o propiedades.

La solución ingenua sería hacer un contenedor global de ellos, como

vector<Vehicle> vehicles;

que se puede acceder desde cualquier lugar.

La solución más amigable para OOP sería hacer que el contenedor sea miembro de la clase que maneja el bucle de eventos principal y que se cree una instancia en su constructor. Cada clase que lo necesite, y sea miembro del hilo principal, tendrá acceso al contenedor a través de un puntero en su constructor. Por ejemplo, si llega un mensaje externo a través de una conexión de red, se hará cargo de una clase (una para cada conexión) que maneja el análisis y el analizador tendrá acceso al contenedor a través de un puntero o referencia. Ahora, si el mensaje analizado produce un cambio en un elemento del contenedor, o requiere algunos datos para realizar una acción, se puede manejar sin la necesidad de arrojar miles de variables a través de señales y ranuras (o peor, almacenarlos en el analizador para luego ser recuperado por quien llamó al analizador). Por supuesto, todas las clases que reciben acceso al contenedor a través de la inyección de dependencia, son parte del mismo hilo. Diferentes hilos no accederán directamente a él, pero harán su trabajo y luego enviarán señales al hilo principal, y las ranuras en el hilo principal actualizarán el contenedor.

Sin embargo, si la mayoría de las clases tendrán acceso al contenedor, ¿qué lo hace realmente diferente de un global? Si tantas clases necesitan los datos en el contenedor, ¿no es la "forma de inyección de dependencias" simplemente un global disfrazado?

Una respuesta sería la seguridad de los subprocesos: aunque me preocupo de no abusar del contenedor global, tal vez otro desarrollador en el futuro, bajo la presión de un plazo cercano, utilice el contenedor global en un subproceso diferente, sin ocuparme de todo Los casos de colisión. Sin embargo, incluso en el caso de la inyección de dependencia, se podría dar un puntero a alguien que se ejecuta en otro hilo, lo que lleva a los mismos problemas.

vsz
fuente
66
Espera, hablas de globales no mutables y usas un enlace a "¿por qué el estado global es malo" para justificarlo? WTF?
Telastyn
1
No estoy justificando a los globales en absoluto. Creo que está claro que estoy de acuerdo con el hecho de que los globales no son la buena solución. Solo estoy hablando de una situación en la que incluso el uso de la inyección de dependencia no puede ser mucho mejor que (o tal vez incluso casi indistinguible) de los globales. Por lo tanto, una respuesta podría señalar otros beneficios ocultos de seguir usando la inyección de dependencia en esta situación, o que otros enfoques serían mejores que di
vsz
99
Pero hay una diferencia decidida entre los globales de solo lectura y el estado global.
Telastyn
1
@vsz no realmente: la pregunta es sobre las fábricas de localización de servicios como una forma de evitar el problema de muchos objetos distintos; pero sus respuestas también responden a su pregunta de permitir que las clases accedan a los datos globales en lugar de que los datos globales se pasen a las clases.
gbjbaanb

Respuestas:

37

en el caso de que casi todos necesiten saber sobre una determinada estructura de datos, ¿por qué la inyección de dependencias es mejor que un objeto global?

La inyección de dependencia es lo mejor desde el pan rebanado , mientras que los objetos globales han sido conocidos por décadas como la fuente de todo mal , por lo que esta es una pregunta bastante interesante.

El punto de inyección de dependencia no es simplemente garantizar que cada actor que necesita algún recurso pueda tenerlo, porque obviamente, si hace que todos los recursos sean globales, entonces cada actor tendrá acceso a cada recurso, problema resuelto, ¿verdad?

El punto de inyección de dependencia es:

  1. Permitir a los actores acceder a los recursos según sea necesario , y
  2. Tener control sobre a qué instancia de un recurso accede un actor determinado.

El hecho de que en su configuración particular todos los actores necesiten acceso a la misma instancia de recurso es irrelevante. Confía en mí, algún día tendrás la necesidad de reconfigurar las cosas para que los actores tengan acceso a diferentes instancias del recurso, y luego te darás cuenta de que te has pintado en una esquina. Algunas respuestas ya han señalado dicha configuración: pruebas .

Otro ejemplo: supongamos que divide su aplicación en cliente-servidor. Todos los actores en el cliente usan el mismo conjunto de recursos centrales en el cliente, y todos los actores en el servidor usan el mismo conjunto de recursos centrales en el servidor. Ahora suponga, un día, que decide crear una versión "independiente" de su aplicación cliente-servidor, donde tanto el cliente como el servidor están empaquetados en un solo ejecutable y se ejecutan en la misma máquina virtual. (O entorno de tiempo de ejecución, según el idioma que elija).

Si usa la inyección de dependencia, puede asegurarse fácilmente de que todos los actores del cliente reciben las instancias de recursos del cliente para trabajar, mientras que todos los actores del servidor reciben las instancias de recursos del servidor.

Si no utiliza la inyección de dependencia, no tiene suerte, ya que solo puede existir una instancia global de cada recurso en una máquina virtual.

Entonces, debe considerar: ¿todos los actores realmente necesitan acceso a ese recurso? ¿De Verdad?

Es posible que haya cometido el error de convertir ese recurso en un objeto divino (por lo que, por supuesto, todos necesitan acceso a él), o tal vez está sobreestimando enormemente la cantidad de actores en su proyecto que realmente necesitan acceso a ese recurso .

Con los globales, cada línea de código fuente en toda su aplicación tiene acceso a cada recurso global. Con la inyección de dependencia, cada instancia de recurso solo es visible para aquellos actores que realmente la necesitan. Si los dos son iguales (los actores que necesitan un recurso en particular comprenden el 100% de las líneas de código fuente en su proyecto), entonces debe haber cometido un error en su diseño. Entonces, tampoco

  • Refactorice ese gran gran recurso de dios enorme en subsistemas más pequeños, por lo que diferentes actores necesitan acceder a diferentes piezas, pero rara vez un actor necesita todas sus piezas, o

  • Refactorice a sus actores para que a su vez acepten como parámetros solo los subconjuntos del problema en el que necesitan trabajar, por lo que no tienen que consultar todo el tiempo un gran recurso central enorme.

Mike Nakis
fuente
No estoy seguro de entender esto: por un lado, usted dice que DI le permite usar varias instancias de un recurso y los globales no lo hacen, lo cual es simplemente una tontería a menos que esté combinando una sola variable estática contra objetos con múltiples instancias, no hay razón que "lo global" debe ser una sola instancia o una sola clase.
gbjbaanb
3
@gbjbaanb Suponga que el recurso es un Zork. El OP dice que no solo cada objeto en su sistema necesita un Zork para trabajar, sino que solo existirá un Zork, y que todos sus objetos solo necesitarán acceso a esa instancia de Zork. Estoy diciendo que ninguno de estos dos supuestos es razonable: probablemente no sea cierto que todos sus objetos necesiten un Zork, y habrá ocasiones en que algunos de los objetos necesitarán una determinada instancia de Zork, mientras que otros objetos necesitarán un diferente instancia de Zork.
Mike Nakis
2
@gbjbaanb No creo que me pidas que explique por qué sería estúpido tener dos instancias globales de Zork, nombrarlas ZorkA y ZorkB, y codificar en los objetos cuál usará ZorkA y cuál usará ZorkB, ¿Derecha?
Mike Nakis
1
No dije a todos, dije a casi todos. El punto de la pregunta era si DI tiene otros beneficios además de negar el acceso de esos pocos actores que no lo necesitan. Sé que los "objetos de Dios" son un antipatrón, pero seguir ciegamente las mejores prácticas también puede serlo. Puede suceder que todo el propósito de un programa sea hacer varias cosas con un determinado recurso, en ese caso casi todos necesitan acceso a ese recurso. Un buen ejemplo sería un programa que funciona en una imagen: casi todo lo que contiene tiene algo que ver con los datos de la imagen.
vsz
1
@gbjbaanb Creo que la idea es tener una clase de cliente capaz de trabajar exclusivamente en ZorkA o ZorkB como se indica, no hacer que la clase de cliente decida cuál de ellos tomar.
Eugene Ryabtsev
39

Hay muchas razones por las cuales los globos no mutables son malos en OOP.

Esa es una afirmación dudosa. El enlace se utiliza como prueba se refiere al estado - mutable globales. Son decididamente malvados. Solo lectura global son constantes. Las constantes son relativamente sanas. Quiero decir, no vas a inyectar el valor de pi en todas tus clases, ¿verdad?

Si el número o el tamaño de los objetos que necesitan compartirse es demasiado grande para pasarlo de manera eficiente en los parámetros de la función, generalmente todos recomiendan la inyección de dependencia en lugar de un objeto global.

No, ellos no.

Si sus funciones / clases tienen demasiadas dependencias, las personas recomiendan detenerse y ver por qué su diseño es tan malo. Tener una gran cantidad de dependencias es una señal de que su clase / función probablemente esté haciendo demasiadas cosas y / o no tenga suficiente abstracción.

Hay una serie de vehículos virtuales que tienen una gran cantidad de propiedades y estados, desde tipo, nombre, color, velocidad, posición, etc. Varios usuarios pueden controlarlos de forma remota y una gran cantidad de eventos (ambos iniciado y automático) puede cambiar muchos de sus estados o propiedades.

Esta es una pesadilla de diseño. Tienes un montón de cosas que se pueden usar / abusar sin ninguna forma de validación, sin limitaciones de concurrencia, es solo un acoplamiento por todas partes.

Sin embargo, si la mayoría de las clases tendrán acceso al contenedor, ¿qué lo hace realmente diferente de un global? Si tantas clases necesitan los datos en el contenedor, ¿no es la "forma de inyección de dependencias" simplemente un global disfrazado?

Sí, tener acoplamiento a datos comunes en todas partes no es muy diferente de un global, y como tal, sigue siendo malo.

Lo bueno es que estás desacoplando a los consumidores de la variable global misma. Esto le proporciona cierta flexibilidad en cómo se crea la instancia. Tampoco te obliga a tener una sola instancia. Puede hacer que se pasen diferentes a sus diferentes consumidores. También puede crear diferentes implementaciones de su interfaz por consumidor si lo necesita.

Y debido al cambio que inevitablemente viene con los requisitos cambiantes del software, un nivel tan básico de flexibilidad es vital .

Telastyn
fuente
perdón por la confusión con "no mutable", quería decir mutable, y de alguna manera no reconocí mi error.
vsz
"Esta es una pesadilla de diseño" - Sé que lo es. Pero si los requisitos son que todos esos eventos deben poder realizar los cambios en los datos, entonces los eventos se unirán a los datos incluso si los divido y los oculto bajo una capa de abstracciones. Todo el programa trata sobre estos datos. Por ejemplo, si un programa tiene que realizar muchas operaciones diferentes en una imagen, entonces todas o casi todas sus clases estarán de alguna manera acopladas a los datos de la imagen. Simplemente decir "escribamos un programa que haga algo diferente" no es aceptable.
vsz
2
Las aplicaciones que tienen muchos objetos que acceden rutinariamente a alguna base de datos no tienen exactamente precedentes.
Robert Harvey
@RobertHarvey: claro, pero ¿la interfaz de la que dependen "puede acceder a una base de datos" o "algún almacén de datos persistente para foos"?
Telastyn
1
Me recuerda a un Dilbert donde PHB pide 500 funciones y Dilbert dice que no. Entonces, PHB pide 1 función y Dilbert dice que sí. Al día siguiente, Dilbert recibe 500 solicitudes cada una para 1 función. Si algo no escala, simplemente no escala sin importar cómo lo vista.
corsiKa
16

Hay tres razones principales que debe considerar.

  1. Legibilidad. Si cada unidad de código tiene todo lo que necesita para trabajar, ya sea inyectado o pasado como parámetro, es fácil mirar el código y ver al instante lo que está haciendo. Esto le da una localidad de función, que también le permite separar mejor las preocupaciones y también lo obliga a pensar en ...
  2. Modularidad. ¿Por qué debería saber el analizador sobre la lista completa de vehículos y todas sus propiedades? Probablemente no. Tal vez deba consultar si existe una identificación del vehículo o si el vehículo X tiene la propiedad Y. Genial, eso significa que puede escribir un servicio que lo haga e inyectar solo ese servicio en su analizador. De repente, terminas con un diseño que tiene mucho más sentido, con cada bit de código que solo maneja datos relevantes para él. Lo que nos lleva a ...
  3. Testabilidad Para una prueba unitaria típica, desea configurar su entorno para muchos escenarios diferentes y la inyección lo hace muy fácil de implementar. Nuevamente, volviendo al ejemplo del analizador, ¿realmente desea crear siempre a mano una lista completa de vehículos completos para cada caso de prueba que escriba para su analizador? ¿O simplemente crearía una implementación simulada del objeto de servicio mencionado anteriormente, devolviendo un número variable de identificadores? Sé cuál elegiría.

En resumen: DI no es un objetivo, es solo una herramienta que hace posible alcanzar un objetivo. Si lo usa incorrectamente (como parece que lo hace en su ejemplo), no se acercará más al objetivo, no hay ninguna propiedad mágica de DI que haga que un mal diseño sea bueno.

biziclop
fuente
+1 para una respuesta concisa centrada en problemas de mantenimiento. Más respuestas P: SE deberían ser como esta.
dodgethesteamroller
1
Su resumen es muy bueno. Tan bueno Si crees que [patrón de diseño del mes] o [palabra de moda del mes] resolverán mágicamente tus problemas, lo pasarás mal.
corsiKa
@corsiKa Fui reacio a DI (o Spring, para ser más precisos) durante mucho tiempo porque no podía ver el punto. Parecía una gran cantidad de problemas sin ganancia tangible (esto fue en los días de configuración XML). Desearía haber encontrado alguna documentación sobre lo que DI te compra en ese momento. Pero no lo hice, así que tuve que resolverlo yo mismo. Llevó mucho tiempo. :)
biziclop
3

Realmente se trata de la capacidad de prueba: normalmente, un objeto global es muy fácil de mantener y fácil de leer / comprender (una vez que sabe que está allí, por supuesto), pero es un elemento permanente y cuando llega a dividir sus clases en pruebas aisladas, descubre que su el objeto global está atascado diciendo "y yo" y, por lo tanto, sus pruebas aisladas requieren que el objeto global se incluya en ellas. No ayuda que la mayoría de los marcos de prueba no admitan fácilmente este escenario.

Entonces sí, sigue siendo un global. La razón fundamental para tenerlo no ha cambiado, solo la forma en que se accede.

En cuanto a la inicialización, esto sigue siendo un problema: aún tiene que establecer la configuración para que sus pruebas puedan ejecutarse, de modo que no obtenga nada allí. Supongo que podría dividir un objeto dependiente grande en muchos más pequeños y pasar el apropiado a las clases que lo necesitan, pero probablemente esté obteniendo aún más complejidad para superar cualquier beneficio.

Independientemente de todo eso, es un ejemplo de la 'cola que menea al perro', la necesidad de probar es determinar cómo está diseñada la base de código (surgen problemas similares con elementos como clases estáticas, por ejemplo, fecha y hora actual).

Personalmente, prefiero pegar todos mis datos globales en un objeto global (o varios) pero proporcionar accesores para obtener los bits que necesitan mis clases. Entonces puedo burlarme de aquellos para devolver los datos que quiero cuando se trata de pruebas sin la complejidad adicional de DI.

gbjbaanb
fuente
"normalmente un objeto global es muy fácil de mantener", no si es mutable. En mi experiencia, tener tanto estado global mutable puede ser una pesadilla (aunque hay formas de hacerlo soportable).
sleske
3

Además de las respuestas que ya tienes,

Hay una serie de vehículos virtuales que tienen una gran cantidad de propiedades y estados, desde tipo, nombre, color, velocidad, posición, etc. Varios usuarios pueden controlarlos de forma remota y una gran cantidad de eventos (ambos iniciado y automático) puede cambiar muchos de sus estados o propiedades.

Una de las características de la programación OOP es mantener solo las propiedades que realmente necesita para un objeto específico,

AHORA SI su objeto tiene demasiadas propiedades: debe dividir su objeto aún más en subobjetos.

Editar

Ya mencioné por qué los objetos globales son malos, le agregaría un ejemplo.

Debe realizar pruebas unitarias en el código GUI de un formulario de Windows, si usa global, deberá crear todos los botones y sus eventos, por ejemplo, para crear un caso de prueba adecuado ...

Matemáticas
fuente
Es cierto, pero por ejemplo, se requerirá que el analizador sepa todo debido a que se puede recibir cualquier comando que pueda hacer cambios en cualquier cosa.
vsz
1
"Antes de llegar a tu pregunta real" ... ¿vas a responder su pregunta?
gbjbaanb
La pregunta de @gbjbaanb tiene mucho texto, permítame un poco de tiempo para leerla correctamente
Matemáticas